/**
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * 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';

var React = require('react');
// TODO: can we express this test with only public API?
var ReactDOMComponentTree = require('../client/ReactDOMComponentTree');
var ReactTestUtils = require('react-dom/test-utils');

/**
 * Ensure that all callbacks are invoked, passing this unique argument.
 */
var ARG = {arg: true};
var ARG2 = {arg2: true};

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>
    );
  }
}

function renderParentIntoDocument() {
  return ReactTestUtils.renderIntoDocument(<ParentComponent />);
}

describe('ReactTreeTraversal', () => {
  var ReactTreeTraversal;
  var mockFn = jest.fn();

  function callback(inst, phase, arg) {
    mockFn(ReactDOMComponentTree.getNodeFromInstance(inst).id, phase, arg);
  }

  function getInst(node) {
    return ReactDOMComponentTree.getInstanceFromNode(node);
  }

  beforeEach(() => {
    ReactTreeTraversal = require('shared/ReactTreeTraversal');
    mockFn.mockReset();
  });

  describe('traverseTwoPhase', () => {
    it('should not traverse when traversing outside DOM', () => {
      ReactTreeTraversal.traverseTwoPhase(null, callback, ARG);
      expect(mockFn).not.toHaveBeenCalled();
    });

    it('should traverse two phase across component boundary', () => {
      var parent = renderParentIntoDocument();
      var target = getInst(parent.refs.P_P1_C1.refs.DIV_1);
      var expectedCalls = [
        ['P', 'captured', ARG],
        ['P_P1', 'captured', ARG],
        ['P_P1_C1__DIV', 'captured', ARG],
        ['P_P1_C1__DIV_1', 'captured', ARG],

        ['P_P1_C1__DIV_1', 'bubbled', ARG],
        ['P_P1_C1__DIV', 'bubbled', ARG],
        ['P_P1', 'bubbled', ARG],
        ['P', 'bubbled', ARG],
      ];
      ReactTreeTraversal.traverseTwoPhase(target, callback, ARG);
      expect(mockFn.mock.calls).toEqual(expectedCalls);
    });

    it('should traverse two phase at shallowest node', () => {
      var parent = renderParentIntoDocument();
      var target = getInst(parent.refs.P);
      var expectedCalls = [['P', 'captured', ARG], ['P', 'bubbled', ARG]];
      ReactTreeTraversal.traverseTwoPhase(target, callback, ARG);
      expect(mockFn.mock.calls).toEqual(expectedCalls);
    });
  });

  describe('traverseEnterLeave', () => {
    it('should not traverse when enter/leaving outside DOM', () => {
      ReactTreeTraversal.traverseEnterLeave(null, null, callback, ARG, ARG2);
      expect(mockFn).not.toHaveBeenCalled();
    });

    it('should not traverse if enter/leave the same node', () => {
      var parent = renderParentIntoDocument();
      var leave = getInst(parent.refs.P_P1_C1.refs.DIV_1);
      var enter = getInst(parent.refs.P_P1_C1.refs.DIV_1);
      ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
      expect(mockFn).not.toHaveBeenCalled();
    });

    it('should traverse enter/leave to sibling - avoids parent', () => {
      var parent = renderParentIntoDocument();
      var leave = getInst(parent.refs.P_P1_C1.refs.DIV_1);
      var enter = getInst(parent.refs.P_P1_C1.refs.DIV_2);
      var expectedCalls = [
        ['P_P1_C1__DIV_1', 'bubbled', ARG],
        // enter/leave shouldn't fire anything on the parent
        ['P_P1_C1__DIV_2', 'captured', ARG2],
      ];
      ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
      expect(mockFn.mock.calls).toEqual(expectedCalls);
    });

    it('should traverse enter/leave to parent - avoids parent', () => {
      var parent = renderParentIntoDocument();
      var leave = getInst(parent.refs.P_P1_C1.refs.DIV_1);
      var enter = getInst(parent.refs.P_P1_C1.refs.DIV);
      var expectedCalls = [['P_P1_C1__DIV_1', 'bubbled', ARG]];
      ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
      expect(mockFn.mock.calls).toEqual(expectedCalls);
    });

    it('should enter from the window', () => {
      var parent = renderParentIntoDocument();
      var leave = null; // From the window or outside of the React sandbox.
      var enter = getInst(parent.refs.P_P1_C1.refs.DIV);
      var expectedCalls = [
        ['P', 'captured', ARG2],
        ['P_P1', 'captured', ARG2],
        ['P_P1_C1__DIV', 'captured', ARG2],
      ];
      ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
      expect(mockFn.mock.calls).toEqual(expectedCalls);
    });

    it('should enter from the window to the shallowest', () => {
      var parent = renderParentIntoDocument();
      var leave = null; // From the window or outside of the React sandbox.
      var enter = getInst(parent.refs.P);
      var expectedCalls = [['P', 'captured', ARG2]];
      ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
      expect(mockFn.mock.calls).toEqual(expectedCalls);
    });

    it('should leave to the window', () => {
      var parent = renderParentIntoDocument();
      var enter = null; // From the window or outside of the React sandbox.
      var leave = getInst(parent.refs.P_P1_C1.refs.DIV);
      var expectedCalls = [
        ['P_P1_C1__DIV', 'bubbled', ARG],
        ['P_P1', 'bubbled', ARG],
        ['P', 'bubbled', ARG],
      ];
      ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
      expect(mockFn.mock.calls).toEqual(expectedCalls);
    });

    it('should leave to the window from the shallowest', () => {
      var parent = renderParentIntoDocument();
      var enter = null; // From the window or outside of the React sandbox.
      var leave = getInst(parent.refs.P_P1_C1.refs.DIV);
      var expectedCalls = [
        ['P_P1_C1__DIV', 'bubbled', ARG],
        ['P_P1', 'bubbled', ARG],
        ['P', 'bubbled', ARG],
      ];
      ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
      expect(mockFn.mock.calls).toEqual(expectedCalls);
    });
  });

  describe('getFirstCommonAncestor', () => {
    it('should determine the first common ancestor correctly', () => {
      var parent = renderParentIntoDocument();
      var 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,
        },
      ];
      var i;
      for (i = 0; i < ancestors.length; i++) {
        var plan = ancestors[i];
        var firstCommon = ReactTreeTraversal.getLowestCommonAncestor(
          getInst(plan.one),
          getInst(plan.two),
        );
        expect(firstCommon).toBe(getInst(plan.com));
      }
    });
  });
});
