/**
 * 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';

let React;
let ReactDOM;
let ReactTestUtils;
let PropTypes;

const clone = function(o) {
  return JSON.parse(JSON.stringify(o));
};

const GET_INIT_STATE_RETURN_VAL = {
  hasWillMountCompleted: false,
  hasRenderCompleted: false,
  hasDidMountCompleted: false,
  hasWillUnmountCompleted: false,
};

const INIT_RENDER_STATE = {
  hasWillMountCompleted: true,
  hasRenderCompleted: false,
  hasDidMountCompleted: false,
  hasWillUnmountCompleted: false,
};

const DID_MOUNT_STATE = {
  hasWillMountCompleted: true,
  hasRenderCompleted: true,
  hasDidMountCompleted: false,
  hasWillUnmountCompleted: false,
};

const NEXT_RENDER_STATE = {
  hasWillMountCompleted: true,
  hasRenderCompleted: true,
  hasDidMountCompleted: true,
  hasWillUnmountCompleted: false,
};

const WILL_UNMOUNT_STATE = {
  hasWillMountCompleted: true,
  hasDidMountCompleted: true,
  hasRenderCompleted: true,
  hasWillUnmountCompleted: false,
};

const POST_WILL_UNMOUNT_STATE = {
  hasWillMountCompleted: true,
  hasDidMountCompleted: true,
  hasRenderCompleted: true,
  hasWillUnmountCompleted: true,
};

/**
 * Every React component is in one of these life cycles.
 */
type ComponentLifeCycle =
  /**
   * Mounted components have a DOM node representation and are capable of
   * receiving new props.
   */
  | 'MOUNTED'
  /**
   * Unmounted components are inactive and cannot receive new props.
   */
  | 'UNMOUNTED';

function getLifeCycleState(instance): ComponentLifeCycle {
  return instance.updater.isMounted(instance) ? 'MOUNTED' : 'UNMOUNTED';
}

/**
 * TODO: We should make any setState calls fail in
 * `getInitialState` and `componentWillMount`. They will usually fail
 * anyways because `this._renderedComponent` is empty, however, if a component
 * is *reused*, then that won't be the case and things will appear to work in
 * some cases. Better to just block all updates in initialization.
 */
describe('ReactComponentLifeCycle', () => {
  beforeEach(() => {
    jest.resetModules();
    React = require('react');
    ReactDOM = require('react-dom');
    ReactTestUtils = require('react-dom/test-utils');
    PropTypes = require('prop-types');
  });

  it('should not reuse an instance when it has been unmounted', () => {
    const container = document.createElement('div');

    class StatefulComponent extends React.Component {
      state = {};

      render() {
        return <div />;
      }
    }

    const element = <StatefulComponent />;
    const firstInstance = ReactDOM.render(element, container);
    ReactDOM.unmountComponentAtNode(container);
    const secondInstance = ReactDOM.render(element, container);
    expect(firstInstance).not.toBe(secondInstance);
  });

  /**
   * If a state update triggers rerendering that in turn fires an onDOMReady,
   * that second onDOMReady should not fail.
   */
  it('it should fire onDOMReady when already in onDOMReady', () => {
    const _testJournal = [];

    class Child extends React.Component {
      componentDidMount() {
        _testJournal.push('Child:onDOMReady');
      }

      render() {
        return <div />;
      }
    }

    class SwitcherParent extends React.Component {
      constructor(props) {
        super(props);
        _testJournal.push('SwitcherParent:getInitialState');
        this.state = {showHasOnDOMReadyComponent: false};
      }

      componentDidMount() {
        _testJournal.push('SwitcherParent:onDOMReady');
        this.switchIt();
      }

      switchIt = () => {
        this.setState({showHasOnDOMReadyComponent: true});
      };

      render() {
        return (
          <div>
            {this.state.showHasOnDOMReadyComponent ? <Child /> : <div />}
          </div>
        );
      }
    }

    ReactTestUtils.renderIntoDocument(<SwitcherParent />);
    expect(_testJournal).toEqual([
      'SwitcherParent:getInitialState',
      'SwitcherParent:onDOMReady',
      'Child:onDOMReady',
    ]);
  });

  // You could assign state here, but not access members of it, unless you
  // had provided a getInitialState method.
  it('throws when accessing state in componentWillMount', () => {
    class StatefulComponent extends React.Component {
      UNSAFE_componentWillMount() {
        void this.state.yada;
      }

      render() {
        return <div />;
      }
    }

    let instance = <StatefulComponent />;
    expect(function() {
      instance = ReactTestUtils.renderIntoDocument(instance);
    }).toThrow();
  });

  it('should allow update state inside of componentWillMount', () => {
    class StatefulComponent extends React.Component {
      UNSAFE_componentWillMount() {
        this.setState({stateField: 'something'});
      }

      render() {
        return <div />;
      }
    }

    let instance = <StatefulComponent />;
    expect(function() {
      instance = ReactTestUtils.renderIntoDocument(instance);
    }).not.toThrow();
  });

  it("warns if setting 'this.state = props'", () => {
    class StatefulComponent extends React.Component {
      constructor(props, context) {
        super(props, context);
        this.state = props;
      }
      render() {
        return <div />;
      }
    }

    expect(() => {
      ReactTestUtils.renderIntoDocument(<StatefulComponent />);
    }).toWarnDev(
      'StatefulComponent: It is not recommended to assign props directly to state ' +
        "because updates to props won't be reflected in state. " +
        'In most cases, it is better to use props directly.',
      {withoutStack: true},
    );
  });

  it('should not allow update state inside of getInitialState', () => {
    class StatefulComponent extends React.Component {
      constructor(props, context) {
        super(props, context);
        this.setState({stateField: 'something'});

        this.state = {stateField: 'somethingelse'};
      }

      render() {
        return <div />;
      }
    }

    expect(() => {
      ReactTestUtils.renderIntoDocument(<StatefulComponent />);
    }).toWarnDev(
      "Warning: Can't call setState on a component that is not yet mounted. " +
        'This is a no-op, but it might indicate a bug in your application. ' +
        'Instead, assign to `this.state` directly or define a `state = {};` ' +
        'class property with the desired state in the StatefulComponent component.',
      {withoutStack: true},
    );

    // Check deduplication; (no extra warnings should be logged).
    ReactTestUtils.renderIntoDocument(<StatefulComponent />);
  });

  it('should correctly determine if a component is mounted', () => {
    class Component extends React.Component {
      _isMounted() {
        // No longer a public API, but we can test that it works internally by
        // reaching into the updater.
        return this.updater.isMounted(this);
      }
      UNSAFE_componentWillMount() {
        expect(this._isMounted()).toBeFalsy();
      }
      componentDidMount() {
        expect(this._isMounted()).toBeTruthy();
      }
      render() {
        expect(this._isMounted()).toBeFalsy();
        return <div />;
      }
    }

    const element = <Component />;

    expect(() => {
      const instance = ReactTestUtils.renderIntoDocument(element);
      expect(instance._isMounted()).toBeTruthy();
    }).toWarnDev('Component is accessing isMounted inside its render()', {
      withoutStack: true,
    });
  });

  it('should correctly determine if a null component is mounted', () => {
    class Component extends React.Component {
      _isMounted() {
        // No longer a public API, but we can test that it works internally by
        // reaching into the updater.
        return this.updater.isMounted(this);
      }
      UNSAFE_componentWillMount() {
        expect(this._isMounted()).toBeFalsy();
      }
      componentDidMount() {
        expect(this._isMounted()).toBeTruthy();
      }
      render() {
        expect(this._isMounted()).toBeFalsy();
        return null;
      }
    }

    const element = <Component />;

    expect(() => {
      const instance = ReactTestUtils.renderIntoDocument(element);
      expect(instance._isMounted()).toBeTruthy();
    }).toWarnDev('Component is accessing isMounted inside its render()', {
      withoutStack: true,
    });
  });

  it('isMounted should return false when unmounted', () => {
    class Component extends React.Component {
      render() {
        return <div />;
      }
    }

    const container = document.createElement('div');
    const instance = ReactDOM.render(<Component />, container);

    // No longer a public API, but we can test that it works internally by
    // reaching into the updater.
    expect(instance.updater.isMounted(instance)).toBe(true);

    ReactDOM.unmountComponentAtNode(container);

    expect(instance.updater.isMounted(instance)).toBe(false);
  });

  it('warns if findDOMNode is used inside render', () => {
    class Component extends React.Component {
      state = {isMounted: false};
      componentDidMount() {
        this.setState({isMounted: true});
      }
      render() {
        if (this.state.isMounted) {
          expect(ReactDOM.findDOMNode(this).tagName).toBe('DIV');
        }
        return <div />;
      }
    }

    expect(() => {
      ReactTestUtils.renderIntoDocument(<Component />);
    }).toWarnDev('Component is accessing findDOMNode inside its render()', {
      withoutStack: true,
    });
  });

  it('should carry through each of the phases of setup', () => {
    class LifeCycleComponent extends React.Component {
      constructor(props, context) {
        super(props, context);
        this._testJournal = {};
        const initState = {
          hasWillMountCompleted: false,
          hasDidMountCompleted: false,
          hasRenderCompleted: false,
          hasWillUnmountCompleted: false,
        };
        this._testJournal.returnedFromGetInitialState = clone(initState);
        this._testJournal.lifeCycleAtStartOfGetInitialState = getLifeCycleState(
          this,
        );
        this.state = initState;
      }

      UNSAFE_componentWillMount() {
        this._testJournal.stateAtStartOfWillMount = clone(this.state);
        this._testJournal.lifeCycleAtStartOfWillMount = getLifeCycleState(this);
        this.state.hasWillMountCompleted = true;
      }

      componentDidMount() {
        this._testJournal.stateAtStartOfDidMount = clone(this.state);
        this._testJournal.lifeCycleAtStartOfDidMount = getLifeCycleState(this);
        this.setState({hasDidMountCompleted: true});
      }

      render() {
        const isInitialRender = !this.state.hasRenderCompleted;
        if (isInitialRender) {
          this._testJournal.stateInInitialRender = clone(this.state);
          this._testJournal.lifeCycleInInitialRender = getLifeCycleState(this);
        } else {
          this._testJournal.stateInLaterRender = clone(this.state);
          this._testJournal.lifeCycleInLaterRender = getLifeCycleState(this);
        }
        // you would *NEVER* do anything like this in real code!
        this.state.hasRenderCompleted = true;
        return <div ref="theDiv">I am the inner DIV</div>;
      }

      componentWillUnmount() {
        this._testJournal.stateAtStartOfWillUnmount = clone(this.state);
        this._testJournal.lifeCycleAtStartOfWillUnmount = getLifeCycleState(
          this,
        );
        this.state.hasWillUnmountCompleted = true;
      }
    }

    // A component that is merely "constructed" (as in "constructor") but not
    // yet initialized, or rendered.
    //
    const container = document.createElement('div');

    let instance;
    expect(() => {
      instance = ReactDOM.render(<LifeCycleComponent />, container);
    }).toWarnDev(
      'LifeCycleComponent is accessing isMounted inside its render() function',
      {withoutStack: true},
    );

    // getInitialState
    expect(instance._testJournal.returnedFromGetInitialState).toEqual(
      GET_INIT_STATE_RETURN_VAL,
    );
    expect(instance._testJournal.lifeCycleAtStartOfGetInitialState).toBe(
      'UNMOUNTED',
    );

    // componentWillMount
    expect(instance._testJournal.stateAtStartOfWillMount).toEqual(
      instance._testJournal.returnedFromGetInitialState,
    );
    expect(instance._testJournal.lifeCycleAtStartOfWillMount).toBe('UNMOUNTED');

    // componentDidMount
    expect(instance._testJournal.stateAtStartOfDidMount).toEqual(
      DID_MOUNT_STATE,
    );
    expect(instance._testJournal.lifeCycleAtStartOfDidMount).toBe('MOUNTED');

    // initial render
    expect(instance._testJournal.stateInInitialRender).toEqual(
      INIT_RENDER_STATE,
    );
    expect(instance._testJournal.lifeCycleInInitialRender).toBe('UNMOUNTED');

    expect(getLifeCycleState(instance)).toBe('MOUNTED');

    // Now *update the component*
    instance.forceUpdate();

    // render 2nd time
    expect(instance._testJournal.stateInLaterRender).toEqual(NEXT_RENDER_STATE);
    expect(instance._testJournal.lifeCycleInLaterRender).toBe('MOUNTED');

    expect(getLifeCycleState(instance)).toBe('MOUNTED');

    ReactDOM.unmountComponentAtNode(container);

    expect(instance._testJournal.stateAtStartOfWillUnmount).toEqual(
      WILL_UNMOUNT_STATE,
    );
    // componentWillUnmount called right before unmount.
    expect(instance._testJournal.lifeCycleAtStartOfWillUnmount).toBe('MOUNTED');

    // But the current lifecycle of the component is unmounted.
    expect(getLifeCycleState(instance)).toBe('UNMOUNTED');
    expect(instance.state).toEqual(POST_WILL_UNMOUNT_STATE);
  });

  it('should not throw when updating an auxiliary component', () => {
    class Tooltip extends React.Component {
      render() {
        return <div>{this.props.children}</div>;
      }

      componentDidMount() {
        this.container = document.createElement('div');
        this.updateTooltip();
      }

      componentDidUpdate() {
        this.updateTooltip();
      }

      updateTooltip = () => {
        // Even though this.props.tooltip has an owner, updating it shouldn't
        // throw here because it's mounted as a root component
        ReactDOM.render(this.props.tooltip, this.container);
      };
    }

    class Component extends React.Component {
      render() {
        return (
          <Tooltip ref="tooltip" tooltip={<div>{this.props.tooltipText}</div>}>
            {this.props.text}
          </Tooltip>
        );
      }
    }

    const container = document.createElement('div');
    ReactDOM.render(<Component text="uno" tooltipText="one" />, container);

    // Since `instance` is a root component, we can set its props. This also
    // makes Tooltip rerender the tooltip component, which shouldn't throw.
    ReactDOM.render(<Component text="dos" tooltipText="two" />, container);
  });

  it('should allow state updates in componentDidMount', () => {
    /**
     * calls setState in an componentDidMount.
     */
    class SetStateInComponentDidMount extends React.Component {
      state = {
        stateField: this.props.valueToUseInitially,
      };

      componentDidMount() {
        this.setState({stateField: this.props.valueToUseInOnDOMReady});
      }

      render() {
        return <div />;
      }
    }

    let instance = (
      <SetStateInComponentDidMount
        valueToUseInitially="hello"
        valueToUseInOnDOMReady="goodbye"
      />
    );
    instance = ReactTestUtils.renderIntoDocument(instance);
    expect(instance.state.stateField).toBe('goodbye');
  });

  it('should call nested legacy lifecycle methods in the right order', () => {
    let log;
    const logger = function(msg) {
      return function() {
        // return true for shouldComponentUpdate
        log.push(msg);
        return true;
      };
    };
    class Outer extends React.Component {
      UNSAFE_componentWillMount = logger('outer componentWillMount');
      componentDidMount = logger('outer componentDidMount');
      UNSAFE_componentWillReceiveProps = logger(
        'outer componentWillReceiveProps',
      );
      shouldComponentUpdate = logger('outer shouldComponentUpdate');
      UNSAFE_componentWillUpdate = logger('outer componentWillUpdate');
      componentDidUpdate = logger('outer componentDidUpdate');
      componentWillUnmount = logger('outer componentWillUnmount');
      render() {
        return (
          <div>
            <Inner x={this.props.x} />
          </div>
        );
      }
    }

    class Inner extends React.Component {
      UNSAFE_componentWillMount = logger('inner componentWillMount');
      componentDidMount = logger('inner componentDidMount');
      UNSAFE_componentWillReceiveProps = logger(
        'inner componentWillReceiveProps',
      );
      shouldComponentUpdate = logger('inner shouldComponentUpdate');
      UNSAFE_componentWillUpdate = logger('inner componentWillUpdate');
      componentDidUpdate = logger('inner componentDidUpdate');
      componentWillUnmount = logger('inner componentWillUnmount');
      render() {
        return <span>{this.props.x}</span>;
      }
    }

    const container = document.createElement('div');
    log = [];
    ReactDOM.render(<Outer x={1} />, container);
    expect(log).toEqual([
      'outer componentWillMount',
      'inner componentWillMount',
      'inner componentDidMount',
      'outer componentDidMount',
    ]);

    // Dedup warnings
    log = [];
    ReactDOM.render(<Outer x={2} />, container);
    expect(log).toEqual([
      'outer componentWillReceiveProps',
      'outer shouldComponentUpdate',
      'outer componentWillUpdate',
      'inner componentWillReceiveProps',
      'inner shouldComponentUpdate',
      'inner componentWillUpdate',
      'inner componentDidUpdate',
      'outer componentDidUpdate',
    ]);

    log = [];
    ReactDOM.unmountComponentAtNode(container);
    expect(log).toEqual([
      'outer componentWillUnmount',
      'inner componentWillUnmount',
    ]);
  });

  it('should call nested new lifecycle methods in the right order', () => {
    let log;
    const logger = function(msg) {
      return function() {
        // return true for shouldComponentUpdate
        log.push(msg);
        return true;
      };
    };
    class Outer extends React.Component {
      state = {};
      static getDerivedStateFromProps(props, prevState) {
        log.push('outer getDerivedStateFromProps');
        return null;
      }
      componentDidMount = logger('outer componentDidMount');
      shouldComponentUpdate = logger('outer shouldComponentUpdate');
      getSnapshotBeforeUpdate = logger('outer getSnapshotBeforeUpdate');
      componentDidUpdate = logger('outer componentDidUpdate');
      componentWillUnmount = logger('outer componentWillUnmount');
      render() {
        return (
          <div>
            <Inner x={this.props.x} />
          </div>
        );
      }
    }

    class Inner extends React.Component {
      state = {};
      static getDerivedStateFromProps(props, prevState) {
        log.push('inner getDerivedStateFromProps');
        return null;
      }
      componentDidMount = logger('inner componentDidMount');
      shouldComponentUpdate = logger('inner shouldComponentUpdate');
      getSnapshotBeforeUpdate = logger('inner getSnapshotBeforeUpdate');
      componentDidUpdate = logger('inner componentDidUpdate');
      componentWillUnmount = logger('inner componentWillUnmount');
      render() {
        return <span>{this.props.x}</span>;
      }
    }

    const container = document.createElement('div');
    log = [];
    ReactDOM.render(<Outer x={1} />, container);
    expect(log).toEqual([
      'outer getDerivedStateFromProps',
      'inner getDerivedStateFromProps',
      'inner componentDidMount',
      'outer componentDidMount',
    ]);

    // Dedup warnings
    log = [];
    ReactDOM.render(<Outer x={2} />, container);
    expect(log).toEqual([
      'outer getDerivedStateFromProps',
      'outer shouldComponentUpdate',
      'inner getDerivedStateFromProps',
      'inner shouldComponentUpdate',
      'inner getSnapshotBeforeUpdate',
      'outer getSnapshotBeforeUpdate',
      'inner componentDidUpdate',
      'outer componentDidUpdate',
    ]);

    log = [];
    ReactDOM.unmountComponentAtNode(container);
    expect(log).toEqual([
      'outer componentWillUnmount',
      'inner componentWillUnmount',
    ]);
  });

  it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => {
    class Component extends React.Component {
      state = {};
      static getDerivedStateFromProps() {
        return null;
      }
      componentWillMount() {
        throw Error('unexpected');
      }
      componentWillReceiveProps() {
        throw Error('unexpected');
      }
      componentWillUpdate() {
        throw Error('unexpected');
      }
      render() {
        return null;
      }
    }

    const container = document.createElement('div');
    expect(() => {
      expect(() => ReactDOM.render(<Component />, container)).toWarnDev(
        'Unsafe legacy lifecycles will not be called for components using new component APIs.',
        {withoutStack: true},
      );
    }).toLowPriorityWarnDev(
      [
        'componentWillMount is deprecated',
        'componentWillReceiveProps is deprecated',
        'componentWillUpdate is deprecated',
      ],
      {withoutStack: true},
    );
  });

  it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', () => {
    class Component extends React.Component {
      state = {};
      getSnapshotBeforeUpdate() {
        return null;
      }
      componentWillMount() {
        throw Error('unexpected');
      }
      componentWillReceiveProps() {
        throw Error('unexpected');
      }
      componentWillUpdate() {
        throw Error('unexpected');
      }
      componentDidUpdate() {}
      render() {
        return null;
      }
    }

    const container = document.createElement('div');
    expect(() => {
      expect(() =>
        ReactDOM.render(<Component value={1} />, container),
      ).toWarnDev(
        'Unsafe legacy lifecycles will not be called for components using new component APIs.',
        {withoutStack: true},
      );
    }).toLowPriorityWarnDev(
      [
        'componentWillMount is deprecated',
        'componentWillReceiveProps is deprecated',
        'componentWillUpdate is deprecated',
      ],
      {withoutStack: true},
    );
    ReactDOM.render(<Component value={2} />, container);
  });

  it('should not invoke new unsafe lifecycles (cWM/cWRP/cWU) if static gDSFP is present', () => {
    class Component extends React.Component {
      state = {};
      static getDerivedStateFromProps() {
        return null;
      }
      UNSAFE_componentWillMount() {
        throw Error('unexpected');
      }
      UNSAFE_componentWillReceiveProps() {
        throw Error('unexpected');
      }
      UNSAFE_componentWillUpdate() {
        throw Error('unexpected');
      }
      render() {
        return null;
      }
    }

    const container = document.createElement('div');
    expect(() => ReactDOM.render(<Component value={1} />, container)).toWarnDev(
      'Unsafe legacy lifecycles will not be called for components using new component APIs.',
      {withoutStack: true},
    );
    ReactDOM.render(<Component value={2} />, container);
  });

  it('should warn about deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => {
    const container = document.createElement('div');

    class AllLegacyLifecycles extends React.Component {
      state = {};
      static getDerivedStateFromProps() {
        return null;
      }
      componentWillMount() {}
      UNSAFE_componentWillReceiveProps() {}
      componentWillUpdate() {}
      render() {
        return null;
      }
    }

    expect(() => {
      expect(() =>
        ReactDOM.render(<AllLegacyLifecycles />, container),
      ).toWarnDev(
        'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
          'AllLegacyLifecycles uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
          '  componentWillMount\n' +
          '  UNSAFE_componentWillReceiveProps\n' +
          '  componentWillUpdate\n\n' +
          'The above lifecycles should be removed. Learn more about this warning here:\n' +
          'https://fb.me/react-async-component-lifecycle-hooks',
        {withoutStack: true},
      );
    }).toLowPriorityWarnDev(
      ['componentWillMount is deprecated', 'componentWillUpdate is deprecated'],
      {withoutStack: true},
    );

    class WillMount extends React.Component {
      state = {};
      static getDerivedStateFromProps() {
        return null;
      }
      UNSAFE_componentWillMount() {}
      render() {
        return null;
      }
    }

    expect(() => ReactDOM.render(<WillMount />, container)).toWarnDev(
      'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
        'WillMount uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
        '  UNSAFE_componentWillMount\n\n' +
        'The above lifecycles should be removed. Learn more about this warning here:\n' +
        'https://fb.me/react-async-component-lifecycle-hooks',
      {withoutStack: true},
    );

    class WillMountAndUpdate extends React.Component {
      state = {};
      static getDerivedStateFromProps() {
        return null;
      }
      componentWillMount() {}
      UNSAFE_componentWillUpdate() {}
      render() {
        return null;
      }
    }

    expect(() => {
      expect(() =>
        ReactDOM.render(<WillMountAndUpdate />, container),
      ).toWarnDev(
        'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
          'WillMountAndUpdate uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
          '  componentWillMount\n' +
          '  UNSAFE_componentWillUpdate\n\n' +
          'The above lifecycles should be removed. Learn more about this warning here:\n' +
          'https://fb.me/react-async-component-lifecycle-hooks',
        {withoutStack: true},
      );
    }).toLowPriorityWarnDev(['componentWillMount is deprecated'], {
      withoutStack: true,
    });

    class WillReceiveProps extends React.Component {
      state = {};
      static getDerivedStateFromProps() {
        return null;
      }
      componentWillReceiveProps() {}
      render() {
        return null;
      }
    }

    expect(() => {
      expect(() => ReactDOM.render(<WillReceiveProps />, container)).toWarnDev(
        'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
          'WillReceiveProps uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
          '  componentWillReceiveProps\n\n' +
          'The above lifecycles should be removed. Learn more about this warning here:\n' +
          'https://fb.me/react-async-component-lifecycle-hooks',
        {withoutStack: true},
      );
    }).toLowPriorityWarnDev(['componentWillReceiveProps is deprecated'], {
      withoutStack: true,
    });
  });

  it('should warn about deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', () => {
    const container = document.createElement('div');

    class AllLegacyLifecycles extends React.Component {
      state = {};
      getSnapshotBeforeUpdate() {}
      componentWillMount() {}
      UNSAFE_componentWillReceiveProps() {}
      componentWillUpdate() {}
      componentDidUpdate() {}
      render() {
        return null;
      }
    }

    expect(() => {
      expect(() =>
        ReactDOM.render(<AllLegacyLifecycles />, container),
      ).toWarnDev(
        'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
          'AllLegacyLifecycles uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
          '  componentWillMount\n' +
          '  UNSAFE_componentWillReceiveProps\n' +
          '  componentWillUpdate\n\n' +
          'The above lifecycles should be removed. Learn more about this warning here:\n' +
          'https://fb.me/react-async-component-lifecycle-hooks',
        {withoutStack: true},
      );
    }).toLowPriorityWarnDev(
      ['componentWillMount is deprecated', 'componentWillUpdate is deprecated'],
      {withoutStack: true},
    );

    class WillMount extends React.Component {
      state = {};
      getSnapshotBeforeUpdate() {}
      UNSAFE_componentWillMount() {}
      componentDidUpdate() {}
      render() {
        return null;
      }
    }

    expect(() => ReactDOM.render(<WillMount />, container)).toWarnDev(
      'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
        'WillMount uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
        '  UNSAFE_componentWillMount\n\n' +
        'The above lifecycles should be removed. Learn more about this warning here:\n' +
        'https://fb.me/react-async-component-lifecycle-hooks',
      {withoutStack: true},
    );

    class WillMountAndUpdate extends React.Component {
      state = {};
      getSnapshotBeforeUpdate() {}
      componentWillMount() {}
      UNSAFE_componentWillUpdate() {}
      componentDidUpdate() {}
      render() {
        return null;
      }
    }

    expect(() => {
      expect(() =>
        ReactDOM.render(<WillMountAndUpdate />, container),
      ).toWarnDev(
        'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
          'WillMountAndUpdate uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
          '  componentWillMount\n' +
          '  UNSAFE_componentWillUpdate\n\n' +
          'The above lifecycles should be removed. Learn more about this warning here:\n' +
          'https://fb.me/react-async-component-lifecycle-hooks',
        {withoutStack: true},
      );
    }).toLowPriorityWarnDev(['componentWillMount is deprecated'], {
      withoutStack: true,
    });

    class WillReceiveProps extends React.Component {
      state = {};
      getSnapshotBeforeUpdate() {}
      componentWillReceiveProps() {}
      componentDidUpdate() {}
      render() {
        return null;
      }
    }

    expect(() => {
      expect(() => ReactDOM.render(<WillReceiveProps />, container)).toWarnDev(
        'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
          'WillReceiveProps uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
          '  componentWillReceiveProps\n\n' +
          'The above lifecycles should be removed. Learn more about this warning here:\n' +
          'https://fb.me/react-async-component-lifecycle-hooks',
        {withoutStack: true},
      );
    }).toLowPriorityWarnDev(['componentWillReceiveProps is deprecated'], {
      withoutStack: true,
    });
  });

  it('calls effects on module-pattern component', function() {
    const log = [];

    function Parent() {
      return {
        render() {
          expect(typeof this.props).toBe('object');
          log.push('render');
          return <Child />;
        },
        UNSAFE_componentWillMount() {
          log.push('will mount');
        },
        componentDidMount() {
          log.push('did mount');
        },
        componentDidUpdate() {
          log.push('did update');
        },
        getChildContext() {
          return {x: 2};
        },
      };
    }
    Parent.childContextTypes = {
      x: PropTypes.number,
    };
    function Child(props, context) {
      expect(context.x).toBe(2);
      return <div />;
    }
    Child.contextTypes = {
      x: PropTypes.number,
    };

    const div = document.createElement('div');
    expect(() =>
      ReactDOM.render(<Parent ref={c => c && log.push('ref')} />, div),
    ).toWarnDev(
      'Warning: The <Parent /> component appears to be a function component that returns a class instance. ' +
        'Change Parent to a class that extends React.Component instead. ' +
        "If you can't use a class try assigning the prototype on the function as a workaround. " +
        '`Parent.prototype = React.Component.prototype`. ' +
        "Don't use an arrow function since it cannot be called with `new` by React.",
      {withoutStack: true},
    );
    ReactDOM.render(<Parent ref={c => c && log.push('ref')} />, div);

    expect(log).toEqual([
      'will mount',
      'render',
      'did mount',
      'ref',

      'render',
      'did update',
      'ref',
    ]);
  });

  it('should warn if getDerivedStateFromProps returns undefined', () => {
    class MyComponent extends React.Component {
      state = {};
      static getDerivedStateFromProps() {}
      render() {
        return null;
      }
    }

    const div = document.createElement('div');
    expect(() => ReactDOM.render(<MyComponent />, div)).toWarnDev(
      'MyComponent.getDerivedStateFromProps(): A valid state object (or null) must ' +
        'be returned. You have returned undefined.',
      {withoutStack: true},
    );

    // De-duped
    ReactDOM.render(<MyComponent />, div);
  });

  it('should warn if state is not initialized before getDerivedStateFromProps', () => {
    class MyComponent extends React.Component {
      static getDerivedStateFromProps() {
        return null;
      }
      render() {
        return null;
      }
    }

    const div = document.createElement('div');
    expect(() => ReactDOM.render(<MyComponent />, div)).toWarnDev(
      '`MyComponent` uses `getDerivedStateFromProps` but its initial state is ' +
        'undefined. This is not recommended. Instead, define the initial state by ' +
        'assigning an object to `this.state` in the constructor of `MyComponent`. ' +
        'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.',
      {withoutStack: true},
    );

    // De-duped
    ReactDOM.render(<MyComponent />, div);
  });

  it('should invoke both deprecated and new lifecycles if both are present', () => {
    const log = [];

    class MyComponent extends React.Component {
      componentWillMount() {
        log.push('componentWillMount');
      }
      componentWillReceiveProps() {
        log.push('componentWillReceiveProps');
      }
      componentWillUpdate() {
        log.push('componentWillUpdate');
      }
      UNSAFE_componentWillMount() {
        log.push('UNSAFE_componentWillMount');
      }
      UNSAFE_componentWillReceiveProps() {
        log.push('UNSAFE_componentWillReceiveProps');
      }
      UNSAFE_componentWillUpdate() {
        log.push('UNSAFE_componentWillUpdate');
      }
      render() {
        return null;
      }
    }

    const div = document.createElement('div');
    expect(() =>
      ReactDOM.render(<MyComponent foo="bar" />, div),
    ).toLowPriorityWarnDev(
      [
        'componentWillMount is deprecated',
        'componentWillReceiveProps is deprecated',
        'componentWillUpdate is deprecated',
      ],
      {withoutStack: true},
    );
    expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']);

    log.length = 0;

    ReactDOM.render(<MyComponent foo="baz" />, div);
    expect(log).toEqual([
      'componentWillReceiveProps',
      'UNSAFE_componentWillReceiveProps',
      'componentWillUpdate',
      'UNSAFE_componentWillUpdate',
    ]);
  });

  it('should not override state with stale values if prevState is spread within getDerivedStateFromProps', () => {
    const divRef = React.createRef();
    let childInstance;

    class Child extends React.Component {
      state = {local: 0};
      static getDerivedStateFromProps(nextProps, prevState) {
        return {...prevState, remote: nextProps.remote};
      }
      updateState = () => {
        this.setState(state => ({local: state.local + 1}));
        this.props.onChange(this.state.remote + 1);
      };
      render() {
        childInstance = this;
        return (
          <div onClick={this.updateState} ref={divRef}>{`remote:${
            this.state.remote
          }, local:${this.state.local}`}</div>
        );
      }
    }

    class Parent extends React.Component {
      state = {value: 0};
      handleChange = value => {
        this.setState({value});
      };
      render() {
        return <Child remote={this.state.value} onChange={this.handleChange} />;
      }
    }

    const container = document.createElement('div');
    document.body.appendChild(container);
    try {
      ReactDOM.render(<Parent />, container);
      expect(divRef.current.textContent).toBe('remote:0, local:0');

      // Trigger setState() calls
      childInstance.updateState();
      expect(divRef.current.textContent).toBe('remote:1, local:1');

      // Trigger batched setState() calls
      divRef.current.click();
      expect(divRef.current.textContent).toBe('remote:2, local:2');
    } finally {
      document.body.removeChild(container);
    }
  });

  it('should pass the return value from getSnapshotBeforeUpdate to componentDidUpdate', () => {
    const log = [];

    class MyComponent extends React.Component {
      state = {
        value: 0,
      };
      static getDerivedStateFromProps(nextProps, prevState) {
        return {
          value: prevState.value + 1,
        };
      }
      getSnapshotBeforeUpdate(prevProps, prevState) {
        log.push(
          `getSnapshotBeforeUpdate() prevProps:${prevProps.value} prevState:${
            prevState.value
          }`,
        );
        return 'abc';
      }
      componentDidUpdate(prevProps, prevState, snapshot) {
        log.push(
          `componentDidUpdate() prevProps:${prevProps.value} prevState:${
            prevState.value
          } snapshot:${snapshot}`,
        );
      }
      render() {
        log.push('render');
        return null;
      }
    }

    const div = document.createElement('div');
    ReactDOM.render(
      <div>
        <MyComponent value="foo" />
      </div>,
      div,
    );
    expect(log).toEqual(['render']);
    log.length = 0;

    ReactDOM.render(
      <div>
        <MyComponent value="bar" />
      </div>,
      div,
    );
    expect(log).toEqual([
      'render',
      'getSnapshotBeforeUpdate() prevProps:foo prevState:1',
      'componentDidUpdate() prevProps:foo prevState:1 snapshot:abc',
    ]);
    log.length = 0;

    ReactDOM.render(
      <div>
        <MyComponent value="baz" />
      </div>,
      div,
    );
    expect(log).toEqual([
      'render',
      'getSnapshotBeforeUpdate() prevProps:bar prevState:2',
      'componentDidUpdate() prevProps:bar prevState:2 snapshot:abc',
    ]);
    log.length = 0;

    ReactDOM.render(<div />, div);
    expect(log).toEqual([]);
  });

  it('should pass previous state to shouldComponentUpdate even with getDerivedStateFromProps', () => {
    const divRef = React.createRef();
    class SimpleComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          value: props.value,
        };
      }

      static getDerivedStateFromProps(nextProps, prevState) {
        if (nextProps.value === prevState.value) {
          return null;
        }
        return {value: nextProps.value};
      }

      shouldComponentUpdate(nextProps, nextState) {
        return nextState.value !== this.state.value;
      }

      render() {
        return <div ref={divRef}>value: {this.state.value}</div>;
      }
    }

    const div = document.createElement('div');

    ReactDOM.render(<SimpleComponent value="initial" />, div);
    expect(divRef.current.textContent).toBe('value: initial');
    ReactDOM.render(<SimpleComponent value="updated" />, div);
    expect(divRef.current.textContent).toBe('value: updated');
  });

  it('should call getSnapshotBeforeUpdate before mutations are committed', () => {
    const log = [];

    class MyComponent extends React.Component {
      divRef = React.createRef();
      getSnapshotBeforeUpdate(prevProps, prevState) {
        log.push('getSnapshotBeforeUpdate');
        expect(this.divRef.current.textContent).toBe(
          `value:${prevProps.value}`,
        );
        return 'foobar';
      }
      componentDidUpdate(prevProps, prevState, snapshot) {
        log.push('componentDidUpdate');
        expect(this.divRef.current.textContent).toBe(
          `value:${this.props.value}`,
        );
        expect(snapshot).toBe('foobar');
      }
      render() {
        log.push('render');
        return <div ref={this.divRef}>{`value:${this.props.value}`}</div>;
      }
    }

    const div = document.createElement('div');
    ReactDOM.render(<MyComponent value="foo" />, div);
    expect(log).toEqual(['render']);
    log.length = 0;

    ReactDOM.render(<MyComponent value="bar" />, div);
    expect(log).toEqual([
      'render',
      'getSnapshotBeforeUpdate',
      'componentDidUpdate',
    ]);
    log.length = 0;
  });

  it('should warn if getSnapshotBeforeUpdate returns undefined', () => {
    class MyComponent extends React.Component {
      getSnapshotBeforeUpdate() {}
      componentDidUpdate() {}
      render() {
        return null;
      }
    }

    const div = document.createElement('div');
    ReactDOM.render(<MyComponent value="foo" />, div);
    expect(() => ReactDOM.render(<MyComponent value="bar" />, div)).toWarnDev(
      'MyComponent.getSnapshotBeforeUpdate(): A snapshot value (or null) must ' +
        'be returned. You have returned undefined.',
      {withoutStack: true},
    );

    // De-duped
    ReactDOM.render(<MyComponent value="baz" />, div);
  });

  it('should warn if getSnapshotBeforeUpdate is defined with no componentDidUpdate', () => {
    class MyComponent extends React.Component {
      getSnapshotBeforeUpdate() {
        return null;
      }
      render() {
        return null;
      }
    }

    const div = document.createElement('div');
    expect(() => ReactDOM.render(<MyComponent />, div)).toWarnDev(
      'MyComponent: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' +
        'This component defines getSnapshotBeforeUpdate() only.',
      {withoutStack: true},
    );

    // De-duped
    ReactDOM.render(<MyComponent />, div);
  });

  it('warns about deprecated unsafe lifecycles', function() {
    class MyComponent extends React.Component {
      componentWillMount() {}
      componentWillReceiveProps() {}
      componentWillUpdate() {}
      render() {
        return null;
      }
    }

    const container = document.createElement('div');
    expect(() =>
      ReactDOM.render(<MyComponent x={1} />, container),
    ).toLowPriorityWarnDev(
      [
        'componentWillMount is deprecated and will be removed in the next major version. ' +
          'Use componentDidMount instead. As a temporary workaround, ' +
          'you can rename to UNSAFE_componentWillMount.' +
          '\n\nPlease update the following components: MyComponent',
        'componentWillReceiveProps is deprecated and will be removed in the next major version. ' +
          'Use static getDerivedStateFromProps instead.' +
          '\n\nPlease update the following components: MyComponent',
        'componentWillUpdate is deprecated and will be removed in the next major version. ' +
          'Use componentDidUpdate instead. As a temporary workaround, ' +
          'you can rename to UNSAFE_componentWillUpdate.' +
          '\n\nPlease update the following components: MyComponent',
      ],
      {withoutStack: true},
    );

    // Dedupe check (update and instantiate new
    ReactDOM.render(<MyComponent x={2} />, container);
    ReactDOM.render(<MyComponent key="new" x={1} />, container);
  });

  describe('react-lifecycles-compat', () => {
    const {polyfill} = require('react-lifecycles-compat');

    it('should not warn for components with polyfilled getDerivedStateFromProps', () => {
      class PolyfilledComponent extends React.Component {
        state = {};
        static getDerivedStateFromProps() {
          return null;
        }
        render() {
          return null;
        }
      }

      polyfill(PolyfilledComponent);

      const container = document.createElement('div');
      ReactDOM.render(
        <React.StrictMode>
          <PolyfilledComponent />
        </React.StrictMode>,
        container,
      );
    });

    it('should not warn for components with polyfilled getSnapshotBeforeUpdate', () => {
      class PolyfilledComponent extends React.Component {
        getSnapshotBeforeUpdate() {
          return null;
        }
        componentDidUpdate() {}
        render() {
          return null;
        }
      }

      polyfill(PolyfilledComponent);

      const container = document.createElement('div');
      ReactDOM.render(
        <React.StrictMode>
          <PolyfilledComponent />
        </React.StrictMode>,
        container,
      );
    });
  });
});
