/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @emails react-core
 */

'use strict';

// This test is a hot mess. It heavily uses internals and relies on DOM even
// though the responder plugin is only used in React Native. Sadness ensues.
// The coverage is valuable though, so we will keep it for now.
const {HostComponent} = require('react-reconciler/src/ReactWorkTags');

let EventBatching;
let EventPluginUtils;
let ResponderEventPlugin;

const touch = function(nodeHandle, i) {
  return {target: nodeHandle, identifier: i};
};

function injectComponentTree(ComponentTree) {
  EventPluginUtils.setComponentTree(
    ComponentTree.getFiberCurrentPropsFromNode,
    ComponentTree.getInstanceFromNode,
    ComponentTree.getNodeFromInstance,
  );
}

/**
 * @param {NodeHandle} nodeHandle @see NodeHandle. Handle of target.
 * @param {Array<Touch>} touches All active touches.
 * @param {Array<Touch>} changedTouches Only the touches that have changed.
 * @return {TouchEvent} Model of a touch event that is compliant with responder
 * system plugin.
 */
const touchEvent = function(nodeHandle, touches, changedTouches) {
  return {
    target: nodeHandle,
    changedTouches: changedTouches,
    touches: touches,
  };
};

const subsequence = function(arr, indices) {
  const ret = [];
  for (let i = 0; i < indices.length; i++) {
    const index = indices[i];
    ret.push(arr[index]);
  }
  return ret;
};

const antiSubsequence = function(arr, indices) {
  const ret = [];
  for (let i = 0; i < arr.length; i++) {
    if (indices.indexOf(i) === -1) {
      ret.push(arr[i]);
    }
  }
  return ret;
};

/**
 * Helper for creating touch test config data.
 * @param allTouchHandles
 */
const _touchConfig = function(
  topType,
  targetNodeHandle,
  allTouchHandles,
  changedIndices,
  eventTarget,
) {
  const allTouchObjects = allTouchHandles.map(touch);
  const changedTouchObjects = subsequence(allTouchObjects, changedIndices);
  const activeTouchObjects =
    topType === 'topTouchStart'
      ? allTouchObjects
      : topType === 'topTouchMove'
      ? allTouchObjects
      : topType === 'topTouchEnd'
      ? antiSubsequence(allTouchObjects, changedIndices)
      : topType === 'topTouchCancel'
      ? antiSubsequence(allTouchObjects, changedIndices)
      : null;

  return {
    nativeEvent: touchEvent(
      targetNodeHandle,
      activeTouchObjects,
      changedTouchObjects,
    ),
    topLevelType: topType,
    targetInst: getInstanceFromNode(targetNodeHandle),
  };
};

/**
 * Creates test data for touch events using environment agnostic "node
 * handles".
 *
 * @param {NodeHandle} nodeHandle Environment agnostic handle to DOM node.
 * @param {Array<NodeHandle>} allTouchHandles Encoding of all "touches" in the
 * form of a mapping from integer (touch `identifier`) to touch target. This is
 * encoded in array form. Because of this, it is possible for two separate
 * touches (meaning two separate indices) to have the same touch target ID -
 * this corresponds to real world cases where two separate unique touches have
 * the same target. These touches don't just represent all active touches,
 * rather it also includes any touches that are not active, but are in the
 * process of being removed.
 * @param {Array<NodeHandle>} changedIndices Indices of `allTouchHandles` that
 * have changed.
 * @return {object} Config data used by test cases for extracting responder
 * events.
 */
const startConfig = function(nodeHandle, allTouchHandles, changedIndices) {
  return _touchConfig(
    'topTouchStart',
    nodeHandle,
    allTouchHandles,
    changedIndices,
    nodeHandle,
  );
};

/**
 * @see `startConfig`
 */
const moveConfig = function(nodeHandle, allTouchHandles, changedIndices) {
  return _touchConfig(
    'topTouchMove',
    nodeHandle,
    allTouchHandles,
    changedIndices,
    nodeHandle,
  );
};

/**
 * @see `startConfig`
 */
const endConfig = function(nodeHandle, allTouchHandles, changedIndices) {
  return _touchConfig(
    'topTouchEnd',
    nodeHandle,
    allTouchHandles,
    changedIndices,
    nodeHandle,
  );
};

/**
 * Test config for events that aren't negotiation related, but rather result of
 * a negotiation.
 *
 * Returns object of the form:
 *
 *     {
 *       responderReject: {
 *         // Whatever "readableIDToID" was passed in.
 *         grandParent: {order: NA, assertEvent: null, returnVal: blah},
 *         ...
 *         child: {order: NA, assertEvent: null, returnVal: blah},
 *       }
 *       responderGrant: {
 *         grandParent: {order: NA, assertEvent: null, returnVal: blah},
 *         ...
 *         child: {order: NA, assertEvent: null, returnVal: blah}
 *       }
 *       ...
 *     }
 *
 * After this is created, a test case would configure specific event orderings
 * and optional assertions. Anything left with an `order` of `NA` will be
 * required to never be invoked (the test runner will make sure it throws if
 * ever invoked).
 *
 */
const NA = -1;
const oneEventLoopTestConfig = function(readableIDToID) {
  const ret = {
    // Negotiation
    scrollShouldSetResponder: {bubbled: {}, captured: {}},
    startShouldSetResponder: {bubbled: {}, captured: {}},
    moveShouldSetResponder: {bubbled: {}, captured: {}},
    responderTerminationRequest: {},

    // Non-negotiation
    responderReject: {}, // These do not bubble capture.
    responderGrant: {},
    responderStart: {},
    responderMove: {},
    responderTerminate: {},
    responderEnd: {},
    responderRelease: {},
  };
  for (const eventName in ret) {
    for (const readableNodeName in readableIDToID) {
      if (ret[eventName].bubbled) {
        // Two phase
        ret[eventName].bubbled[readableNodeName] = {
          order: NA,
          assertEvent: null,
          returnVal: undefined,
        };
        ret[eventName].captured[readableNodeName] = {
          order: NA,
          assertEvent: null,
          returnVal: undefined,
        };
      } else {
        ret[eventName][readableNodeName] = {
          order: NA,
          assertEvent: null,
          returnVal: undefined,
        };
      }
    }
  }
  return ret;
};

/**
 * @param {object} eventTestConfig
 * @param {object} readableIDToID
 */
const registerTestHandlers = function(eventTestConfig, readableIDToID) {
  const runs = {dispatchCount: 0};
  const neverFire = function(readableID, registrationName) {
    runs.dispatchCount++;
    expect('').toBe(
      'Event type: ' +
        registrationName +
        '\nShould never occur on:' +
        readableID +
        '\nFor event test config:\n' +
        JSON.stringify(eventTestConfig) +
        '\n',
    );
  };

  const registerOneEventType = function(registrationName, eventTypeTestConfig) {
    for (const readableID in eventTypeTestConfig) {
      const nodeConfig = eventTypeTestConfig[readableID];
      const id = readableIDToID[readableID];
      const handler =
        nodeConfig.order === NA
          ? neverFire.bind(null, readableID, registrationName)
          : // We partially apply readableID and nodeConfig, as they change in the
            // parent closure across iterations.
            function(rID, config, e) {
              expect(
                rID +
                  '->' +
                  registrationName +
                  ' index:' +
                  runs.dispatchCount++,
              ).toBe(rID + '->' + registrationName + ' index:' + config.order);
              if (config.assertEvent) {
                config.assertEvent(e);
              }
              return config.returnVal;
            }.bind(null, readableID, nodeConfig);
      putListener(getInstanceFromNode(id), registrationName, handler);
    }
  };
  for (const eventName in eventTestConfig) {
    const oneEventTypeTestConfig = eventTestConfig[eventName];
    const hasTwoPhase = !!oneEventTypeTestConfig.bubbled;
    if (hasTwoPhase) {
      registerOneEventType(
        ResponderEventPlugin.eventTypes[eventName].phasedRegistrationNames
          .bubbled,
        oneEventTypeTestConfig.bubbled,
      );
      registerOneEventType(
        ResponderEventPlugin.eventTypes[eventName].phasedRegistrationNames
          .captured,
        oneEventTypeTestConfig.captured,
      );
    } else {
      registerOneEventType(
        ResponderEventPlugin.eventTypes[eventName].registrationName,
        oneEventTypeTestConfig,
      );
    }
  }
  return runs;
};

const run = function(config, hierarchyConfig, nativeEventConfig) {
  let max = NA;
  const searchForMax = function(nodeConfig) {
    for (const readableID in nodeConfig) {
      const order = nodeConfig[readableID].order;
      max = order > max ? order : max;
    }
  };
  for (const eventName in config) {
    const eventConfig = config[eventName];
    if (eventConfig.bubbled) {
      searchForMax(eventConfig.bubbled);
      searchForMax(eventConfig.captured);
    } else {
      searchForMax(eventConfig);
    }
  }

  // Register the handlers
  const runData = registerTestHandlers(config, hierarchyConfig);

  // Trigger the event
  const extractedEvents = ResponderEventPlugin.extractEvents(
    nativeEventConfig.topLevelType,
    nativeEventConfig.targetInst,
    nativeEventConfig.nativeEvent,
    nativeEventConfig.target,
    0,
  );

  // At this point the negotiation events have been dispatched as part of the
  // extraction process, but not the side effectful events. Below, we dispatch
  // side effectful events.
  EventBatching.runEventsInBatch(extractedEvents);

  // Ensure that every event that declared an `order`, was actually dispatched.
  expect('number of events dispatched:' + runData.dispatchCount).toBe(
    'number of events dispatched:' + (max + 1),
  ); // +1 for extra ++
};

const GRANDPARENT_HOST_NODE = {};
const PARENT_HOST_NODE = {};
const CHILD_HOST_NODE = {};
const CHILD_HOST_NODE2 = {};

// These intentionally look like Fibers. ReactTreeTraversal depends on their field names.
// TODO: we could test this with regular DOM nodes (and real fibers) instead.
const GRANDPARENT_INST = {
  return: null,
  tag: HostComponent,
  stateNode: GRANDPARENT_HOST_NODE,
  memoizedProps: {},
};
const PARENT_INST = {
  return: GRANDPARENT_INST,
  tag: HostComponent,
  stateNode: PARENT_HOST_NODE,
  memoizedProps: {},
};
const CHILD_INST = {
  return: PARENT_INST,
  tag: HostComponent,
  stateNode: CHILD_HOST_NODE,
  memoizedProps: {},
};
const CHILD_INST2 = {
  return: PARENT_INST,
  tag: HostComponent,
  stateNode: CHILD_HOST_NODE2,
  memoizedProps: {},
};

GRANDPARENT_HOST_NODE.testInstance = GRANDPARENT_INST;
PARENT_HOST_NODE.testInstance = PARENT_INST;
CHILD_HOST_NODE.testInstance = CHILD_INST;
CHILD_HOST_NODE2.testInstance = CHILD_INST2;

const three = {
  grandParent: GRANDPARENT_HOST_NODE,
  parent: PARENT_HOST_NODE,
  child: CHILD_HOST_NODE,
};

const siblings = {
  parent: PARENT_HOST_NODE,
  childOne: CHILD_HOST_NODE,
  childTwo: CHILD_HOST_NODE2,
};

function getInstanceFromNode(node) {
  return node.testInstance;
}

function getNodeFromInstance(inst) {
  return inst.stateNode;
}

function getFiberCurrentPropsFromNode(node) {
  return node.testInstance.memoizedProps;
}

function putListener(instance, registrationName, handler) {
  instance.memoizedProps[registrationName] = handler;
}

function deleteAllListeners(instance) {
  instance.memoizedProps = {};
}

describe('ResponderEventPlugin', () => {
  beforeEach(() => {
    jest.resetModules();

    EventBatching = require('react-native-renderer/src/legacy-events/EventBatching');
    EventPluginUtils = require('react-native-renderer/src/legacy-events/EventPluginUtils');
    ResponderEventPlugin = require('react-native-renderer/src/legacy-events/ResponderEventPlugin')
      .default;

    deleteAllListeners(GRANDPARENT_INST);
    deleteAllListeners(PARENT_INST);
    deleteAllListeners(CHILD_INST);
    deleteAllListeners(CHILD_INST2);

    injectComponentTree({
      getInstanceFromNode,
      getNodeFromInstance,
      getFiberCurrentPropsFromNode,
    });
  });

  it('should do nothing when no one wants to respond', () => {
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.child = {
      order: 2,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
    config.startShouldSetResponder.bubbled.parent = {
      order: 4,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.grandParent = {
      order: 5,
      returnVal: false,
    };
    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);

    // Now no handlers should be called on `touchEnd`.
    config = oneEventLoopTestConfig(three);
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  /**
   * Simple Start Granting
   * --------------------
   */

  it('should grant responder grandParent while capturing', () => {
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: true,
    };
    config.responderGrant.grandParent = {order: 1};
    config.responderStart.grandParent = {order: 2};
    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.grandParent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.grandParent = {order: 0};
    config.responderRelease.grandParent = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should grant responder parent while capturing', () => {
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: true,
    };
    config.responderGrant.parent = {order: 2};
    config.responderStart.parent = {order: 3};
    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.parent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.parent = {order: 0};
    config.responderRelease.parent = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should grant responder child while capturing', () => {
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.child = {order: 2, returnVal: true};
    config.responderGrant.child = {order: 3};
    config.responderStart.child = {order: 4};
    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.child = {order: 0};
    config.responderRelease.child = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should grant responder child while bubbling', () => {
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.child = {
      order: 2,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
    config.responderGrant.child = {order: 4};
    config.responderStart.child = {order: 5};
    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.child = {order: 0};
    config.responderRelease.child = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should grant responder parent while bubbling', () => {
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.child = {
      order: 2,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
    config.startShouldSetResponder.bubbled.parent = {order: 4, returnVal: true};
    config.responderGrant.parent = {order: 5};
    config.responderStart.parent = {order: 6};
    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.parent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.parent = {order: 0};
    config.responderRelease.parent = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should grant responder grandParent while bubbling', () => {
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.child = {
      order: 2,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
    config.startShouldSetResponder.bubbled.parent = {
      order: 4,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.grandParent = {
      order: 5,
      returnVal: true,
    };
    config.responderGrant.grandParent = {order: 6};
    config.responderStart.grandParent = {order: 7};
    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.grandParent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.grandParent = {order: 0};
    config.responderRelease.grandParent = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  /**
   * Simple Move Granting
   * --------------------
   */

  it('should grant responder grandParent while capturing move', () => {
    let config = oneEventLoopTestConfig(three);

    config.startShouldSetResponder.captured.grandParent = {order: 0};
    config.startShouldSetResponder.captured.parent = {order: 1};
    config.startShouldSetResponder.captured.child = {order: 2};
    config.startShouldSetResponder.bubbled.child = {order: 3};
    config.startShouldSetResponder.bubbled.parent = {order: 4};
    config.startShouldSetResponder.bubbled.grandParent = {order: 5};
    run(config, three, startConfig(three.child, [three.child], [0]));

    config = oneEventLoopTestConfig(three);
    config.moveShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: true,
    };
    config.responderGrant.grandParent = {order: 1};
    config.responderMove.grandParent = {order: 2};
    run(config, three, moveConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.grandParent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.grandParent = {order: 0};
    config.responderRelease.grandParent = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should grant responder parent while capturing move', () => {
    let config = oneEventLoopTestConfig(three);

    config.startShouldSetResponder.captured.grandParent = {order: 0};
    config.startShouldSetResponder.captured.parent = {order: 1};
    config.startShouldSetResponder.captured.child = {order: 2};
    config.startShouldSetResponder.bubbled.child = {order: 3};
    config.startShouldSetResponder.bubbled.parent = {order: 4};
    config.startShouldSetResponder.bubbled.grandParent = {order: 5};
    run(config, three, startConfig(three.child, [three.child], [0]));

    config = oneEventLoopTestConfig(three);
    config.moveShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: true};
    config.responderGrant.parent = {order: 2};
    config.responderMove.parent = {order: 3};
    run(config, three, moveConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.parent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.parent = {order: 0};
    config.responderRelease.parent = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should grant responder child while capturing move', () => {
    let config = oneEventLoopTestConfig(three);

    config.startShouldSetResponder.captured.grandParent = {order: 0};
    config.startShouldSetResponder.captured.parent = {order: 1};
    config.startShouldSetResponder.captured.child = {order: 2};
    config.startShouldSetResponder.bubbled.child = {order: 3};
    config.startShouldSetResponder.bubbled.parent = {order: 4};
    config.startShouldSetResponder.bubbled.grandParent = {order: 5};
    run(config, three, startConfig(three.child, [three.child], [0]));

    config = oneEventLoopTestConfig(three);
    config.moveShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.child = {order: 2, returnVal: true};
    config.responderGrant.child = {order: 3};
    config.responderMove.child = {order: 4};
    run(config, three, moveConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.child = {order: 0};
    config.responderRelease.child = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should grant responder child while bubbling move', () => {
    let config = oneEventLoopTestConfig(three);

    config.startShouldSetResponder.captured.grandParent = {order: 0};
    config.startShouldSetResponder.captured.parent = {order: 1};
    config.startShouldSetResponder.captured.child = {order: 2};
    config.startShouldSetResponder.bubbled.child = {order: 3};
    config.startShouldSetResponder.bubbled.parent = {order: 4};
    config.startShouldSetResponder.bubbled.grandParent = {order: 5};
    run(config, three, startConfig(three.child, [three.child], [0]));

    config = oneEventLoopTestConfig(three);
    config.moveShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.child = {order: 2, returnVal: false};
    config.moveShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
    config.responderGrant.child = {order: 4};
    config.responderMove.child = {order: 5};
    run(config, three, moveConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.child = {order: 0};
    config.responderRelease.child = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should grant responder parent while bubbling move', () => {
    let config = oneEventLoopTestConfig(three);

    config.startShouldSetResponder.captured.grandParent = {order: 0};
    config.startShouldSetResponder.captured.parent = {order: 1};
    config.startShouldSetResponder.captured.child = {order: 2};
    config.startShouldSetResponder.bubbled.child = {order: 3};
    config.startShouldSetResponder.bubbled.parent = {order: 4};
    config.startShouldSetResponder.bubbled.grandParent = {order: 5};
    run(config, three, startConfig(three.child, [three.child], [0]));

    config = oneEventLoopTestConfig(three);
    config.moveShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.child = {order: 2, returnVal: false};
    config.moveShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
    config.moveShouldSetResponder.bubbled.parent = {order: 4, returnVal: true};
    config.responderGrant.parent = {order: 5};
    config.responderMove.parent = {order: 6};
    run(config, three, moveConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.parent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.parent = {order: 0};
    config.responderRelease.parent = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should grant responder grandParent while bubbling move', () => {
    let config = oneEventLoopTestConfig(three);

    config.startShouldSetResponder.captured.grandParent = {order: 0};
    config.startShouldSetResponder.captured.parent = {order: 1};
    config.startShouldSetResponder.captured.child = {order: 2};
    config.startShouldSetResponder.bubbled.child = {order: 3};
    config.startShouldSetResponder.bubbled.parent = {order: 4};
    config.startShouldSetResponder.bubbled.grandParent = {order: 5};
    run(config, three, startConfig(three.child, [three.child], [0]));

    config = oneEventLoopTestConfig(three);
    config.moveShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.child = {order: 2, returnVal: false};
    config.moveShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
    config.moveShouldSetResponder.bubbled.parent = {order: 4, returnVal: false};
    config.moveShouldSetResponder.bubbled.grandParent = {
      order: 5,
      returnVal: true,
    };
    config.responderGrant.grandParent = {order: 6};
    config.responderMove.grandParent = {order: 7};
    run(config, three, moveConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.grandParent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.grandParent = {order: 0};
    config.responderRelease.grandParent = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  /**
   * Common ancestor tests
   * ---------------------
   */

  it('should bubble negotiation to first common ancestor of responder', () => {
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: true,
    };
    config.responderGrant.parent = {order: 2};
    config.responderStart.parent = {order: 3};
    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.parent),
    );

    // While `parent` is still responder, we create new handlers that verify
    // the ordering of propagation, restarting the count at `0`.
    config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };

    config.startShouldSetResponder.bubbled.grandParent = {
      order: 1,
      returnVal: false,
    };
    config.responderStart.parent = {order: 2};
    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.parent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.parent = {order: 0};
    config.responderRelease.parent = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should bubble negotiation to first common ancestor of responder then transfer', () => {
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: true,
    };
    config.responderGrant.parent = {order: 2};
    config.responderStart.parent = {order: 3};
    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.parent),
    );

    config = oneEventLoopTestConfig(three);

    // Parent is responder, and responder is transferred by a second touch start
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: true,
    };
    config.responderGrant.grandParent = {order: 1};
    config.responderTerminationRequest.parent = {order: 2, returnVal: true};
    config.responderTerminate.parent = {order: 3};
    config.responderStart.grandParent = {order: 4};
    run(
      config,
      three,
      startConfig(three.child, [three.child, three.child], [1]),
    );
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.grandParent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.grandParent = {order: 0};
    // one remains\ /one ended \
    run(config, three, endConfig(three.child, [three.child, three.child], [1]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.grandParent),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.grandParent = {order: 0};
    config.responderRelease.grandParent = {order: 1};
    run(config, three, endConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  /**
   * If nothing is responder, then the negotiation should propagate directly to
   * the deepest target in the second touch.
   */
  it('should negotiate with deepest target on second touch if nothing is responder', () => {
    // Initially nothing wants to become the responder
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.parent = {
      order: 2,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.grandParent = {
      order: 3,
      returnVal: false,
    };

    run(config, three, startConfig(three.parent, [three.parent], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(null);

    config = oneEventLoopTestConfig(three);

    // Now child wants to become responder. Negotiation should bubble as deep
    // as the target is because we don't find first common ancestor (with
    // current responder) because there is no current responder.
    // (Even if this is the second active touch).
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.child = {
      order: 2,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
    config.responderGrant.child = {order: 4};
    config.responderStart.child = {order: 5};
    //                                     /  Two active touches  \  /one of them new\
    run(
      config,
      three,
      startConfig(three.child, [three.parent, three.child], [1]),
    );
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    // Now we remove the original first touch, keeping the second touch that
    // started within the current responder (child). Nothing changes because
    // there's still touches that started inside of the current responder.
    config = oneEventLoopTestConfig(three);
    config.responderEnd.child = {order: 0};
    //                                      / one ended\  /one remains \
    run(
      config,
      three,
      endConfig(three.child, [three.parent, three.child], [0]),
    );
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    // Okay, now let's add back that first touch (nothing should change) and
    // then we'll try peeling back the touches in the opposite order to make
    // sure that first removing the second touch instantly causes responder to
    // be released.
    config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.parent = {
      order: 2,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.grandParent = {
      order: 3,
      returnVal: false,
    };
    // Interesting: child still gets moves even though touch target is parent!
    // Current responder gets a `responderStart` for any touch while responder.
    config.responderStart.child = {order: 4};
    //                                           /  Two active touches  \  /one of them new\
    run(
      config,
      three,
      startConfig(three.parent, [three.child, three.parent], [1]),
    );
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    // Now, move that new touch that had no effect, and did not start within
    // the current responder.
    config = oneEventLoopTestConfig(three);
    config.moveShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.moveShouldSetResponder.bubbled.parent = {order: 2, returnVal: false};
    config.moveShouldSetResponder.bubbled.grandParent = {
      order: 3,
      returnVal: false,
    };
    // Interesting: child still gets moves even though touch target is parent!
    // Current responder gets a `responderMove` for any touch while responder.
    config.responderMove.child = {order: 4};
    //                                     /  Two active touches  \  /one of them moved\
    run(
      config,
      three,
      moveConfig(three.parent, [three.child, three.parent], [1]),
    );
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.child = {order: 0};
    config.responderRelease.child = {order: 1};
    //                                        /child end \ /parent remain\
    run(
      config,
      three,
      endConfig(three.child, [three.child, three.parent], [0]),
    );
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  /**
   * If nothing is responder, then the negotiation should propagate directly to
   * the deepest target in the second touch.
   */
  it('should negotiate until first common ancestor when there are siblings', () => {
    // Initially nothing wants to become the responder
    let config = oneEventLoopTestConfig(siblings);
    config.startShouldSetResponder.captured.parent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.childOne = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.childOne = {
      order: 2,
      returnVal: true,
    };
    config.responderGrant.childOne = {order: 3};
    config.responderStart.childOne = {order: 4};

    run(
      config,
      siblings,
      startConfig(siblings.childOne, [siblings.childOne], [0]),
    );
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(siblings.childOne),
    );

    // If the touch target is the sibling item, the negotiation should only
    // propagate to first common ancestor of current responder and sibling (so
    // the parent).
    config = oneEventLoopTestConfig(siblings);
    config.startShouldSetResponder.captured.parent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.parent = {
      order: 1,
      returnVal: false,
    };
    config.responderStart.childOne = {order: 2};

    const touchConfig = startConfig(
      siblings.childTwo,
      [siblings.childOne, siblings.childTwo],
      [1],
    );
    run(config, siblings, touchConfig);
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(siblings.childOne),
    );

    // move childOne
    config = oneEventLoopTestConfig(siblings);
    config.moveShouldSetResponder.captured.parent = {
      order: 0,
      returnVal: false,
    };
    config.moveShouldSetResponder.bubbled.parent = {order: 1, returnVal: false};
    config.responderMove.childOne = {order: 2};
    run(
      config,
      siblings,
      moveConfig(
        siblings.childOne,
        [siblings.childOne, siblings.childTwo],
        [0],
      ),
    );
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(siblings.childOne),
    );

    // move childTwo: Only negotiates to `parent`.
    config = oneEventLoopTestConfig(siblings);
    config.moveShouldSetResponder.captured.parent = {
      order: 0,
      returnVal: false,
    };
    config.moveShouldSetResponder.bubbled.parent = {order: 1, returnVal: false};
    config.responderMove.childOne = {order: 2};
    run(
      config,
      siblings,
      moveConfig(
        siblings.childTwo,
        [siblings.childOne, siblings.childTwo],
        [1],
      ),
    );
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(siblings.childOne),
    );
  });

  it('should notify of being rejected. responderStart/Move happens on current responder', () => {
    // Initially nothing wants to become the responder
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.child = {
      order: 2,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
    config.responderGrant.child = {order: 4};
    config.responderStart.child = {order: 5};

    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    // Suppose parent wants to become responder on move, and is rejected
    config = oneEventLoopTestConfig(three);
    config.moveShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.moveShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.moveShouldSetResponder.bubbled.parent = {order: 2, returnVal: true};
    config.responderGrant.parent = {order: 3};
    config.responderTerminationRequest.child = {order: 4, returnVal: false};
    config.responderReject.parent = {order: 5};
    // The start/move should occur on the original responder if new one is rejected
    config.responderMove.child = {order: 6};

    let touchConfig = moveConfig(three.child, [three.child], [0]);
    run(config, three, touchConfig);
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.parent = {order: 2, returnVal: true};
    config.responderGrant.parent = {order: 3};
    config.responderTerminationRequest.child = {order: 4, returnVal: false};
    config.responderReject.parent = {order: 5};
    // The start/move should occur on the original responder if new one is rejected
    config.responderStart.child = {order: 6};

    touchConfig = startConfig(three.child, [three.child, three.child], [1]);
    run(config, three, touchConfig);
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );
  });

  it('should negotiate scroll', () => {
    // Initially nothing wants to become the responder
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.child = {
      order: 2,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
    config.responderGrant.child = {order: 4};
    config.responderStart.child = {order: 5};

    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    // If the touch target is the sibling item, the negotiation should only
    // propagate to first common ancestor of current responder and sibling (so
    // the parent).
    config = oneEventLoopTestConfig(three);
    config.scrollShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.scrollShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.scrollShouldSetResponder.bubbled.parent = {
      order: 2,
      returnVal: true,
    };
    config.responderGrant.parent = {order: 3};
    config.responderTerminationRequest.child = {order: 4, returnVal: false};
    config.responderReject.parent = {order: 5};

    run(config, three, {
      topLevelType: 'topScroll',
      targetInst: getInstanceFromNode(three.parent),
      nativeEvent: {},
    });
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    // Now lets let the scroll take control this time.
    config = oneEventLoopTestConfig(three);
    config.scrollShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.scrollShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.scrollShouldSetResponder.bubbled.parent = {
      order: 2,
      returnVal: true,
    };
    config.responderGrant.parent = {order: 3};
    config.responderTerminationRequest.child = {order: 4, returnVal: true};
    config.responderTerminate.child = {order: 5};

    run(config, three, {
      topLevelType: 'topScroll',
      targetInst: getInstanceFromNode(three.parent),
      nativeEvent: {},
    });
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.parent),
    );
  });

  it('should cancel correctly', () => {
    // Initially our child becomes responder
    let config = oneEventLoopTestConfig(three);
    config.startShouldSetResponder.captured.grandParent = {
      order: 0,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.parent = {
      order: 1,
      returnVal: false,
    };
    config.startShouldSetResponder.captured.child = {
      order: 2,
      returnVal: false,
    };
    config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
    config.responderGrant.child = {order: 4};
    config.responderStart.child = {order: 5};

    run(config, three, startConfig(three.child, [three.child], [0]));
    expect(ResponderEventPlugin._getResponder()).toBe(
      getInstanceFromNode(three.child),
    );

    config = oneEventLoopTestConfig(three);
    config.responderEnd.child = {order: 0};
    config.responderTerminate.child = {order: 1};

    const nativeEvent = _touchConfig(
      'topTouchCancel',
      three.child,
      [three.child],
      [0],
    );
    run(config, three, nativeEvent);
    expect(ResponderEventPlugin._getResponder()).toBe(null);
  });

  it('should determine the first common ancestor correctly', () => {
    // This test was moved here from the ReactTreeTraversal test since only the
    // ResponderEventPlugin uses `getLowestCommonAncestor`
    const React = require('react');
    const ReactTestUtils = require('react-dom/test-utils');
    const getLowestCommonAncestor = require('react-native-renderer/src/legacy-events/ResponderEventPlugin')
      .getLowestCommonAncestor;
    // This works by accident and will likely break in the future.
    const ReactDOMComponentTree = require('react-dom/src/client/ReactDOMComponentTree');

    class ChildComponent extends React.Component {
      render() {
        return (
          <div ref="DIV" id={this.props.id + '__DIV'}>
            <div ref="DIV_1" id={this.props.id + '__DIV_1'} />
            <div ref="DIV_2" id={this.props.id + '__DIV_2'} />
          </div>
        );
      }
    }

    class ParentComponent extends React.Component {
      render() {
        return (
          <div ref="P" id="P">
            <div ref="P_P1" id="P_P1">
              <ChildComponent ref="P_P1_C1" id="P_P1_C1" />
              <ChildComponent ref="P_P1_C2" id="P_P1_C2" />
            </div>
            <div ref="P_OneOff" id="P_OneOff" />
          </div>
        );
      }
    }

    const parent = ReactTestUtils.renderIntoDocument(<ParentComponent />);

    const ancestors = [
      // Common ancestor with self is self.
      {
        one: parent.refs.P_P1_C1.refs.DIV_1,
        two: parent.refs.P_P1_C1.refs.DIV_1,
        com: parent.refs.P_P1_C1.refs.DIV_1,
      },
      // Common ancestor with self is self - even if topmost DOM.
      {one: parent.refs.P, two: parent.refs.P, com: parent.refs.P},
      // Siblings
      {
        one: parent.refs.P_P1_C1.refs.DIV_1,
        two: parent.refs.P_P1_C1.refs.DIV_2,
        com: parent.refs.P_P1_C1.refs.DIV,
      },
      // Common ancestor with parent is the parent.
      {
        one: parent.refs.P_P1_C1.refs.DIV_1,
        two: parent.refs.P_P1_C1.refs.DIV,
        com: parent.refs.P_P1_C1.refs.DIV,
      },
      // Common ancestor with grandparent is the grandparent.
      {
        one: parent.refs.P_P1_C1.refs.DIV_1,
        two: parent.refs.P_P1,
        com: parent.refs.P_P1,
      },
      // Grandparent across subcomponent boundaries.
      {
        one: parent.refs.P_P1_C1.refs.DIV_1,
        two: parent.refs.P_P1_C2.refs.DIV_1,
        com: parent.refs.P_P1,
      },
      // Something deep with something one-off.
      {
        one: parent.refs.P_P1_C1.refs.DIV_1,
        two: parent.refs.P_OneOff,
        com: parent.refs.P,
      },
    ];
    let i;
    for (i = 0; i < ancestors.length; i++) {
      const plan = ancestors[i];
      const firstCommon = getLowestCommonAncestor(
        ReactDOMComponentTree.getInstanceFromNode(plan.one),
        ReactDOMComponentTree.getInstanceFromNode(plan.two),
      );
      expect(firstCommon).toBe(
        ReactDOMComponentTree.getInstanceFromNode(plan.com),
      );
    }
  });
});
