/**
 * Copyright (c) Meta Platforms, Inc. and 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 act;
let assertConsoleErrorDev;

let PropTypes;
let React;
let ReactDOMClient;
let createReactClass;

describe('create-react-class-integration', () => {
  beforeEach(() => {
    jest.resetModules();
    ({act, assertConsoleErrorDev} = require('internal-test-utils'));
    PropTypes = require('prop-types');
    React = require('react');
    ReactDOMClient = require('react-dom/client');
    createReactClass = require('create-react-class/factory')(
      React.Component,
      React.isValidElement,
      new React.Component().updater,
    );
  });

  it('should throw when `render` is not specified', () => {
    expect(function () {
      createReactClass({});
    }).toThrowError('Class specification must implement a `render` method.');
  });

  it('should copy prop types onto the Constructor', () => {
    const propValidator = jest.fn();
    const TestComponent = createReactClass({
      propTypes: {
        value: propValidator,
      },
      render: function () {
        return <div />;
      },
    });

    expect(TestComponent.propTypes).toBeDefined();
    expect(TestComponent.propTypes.value).toBe(propValidator);
  });

  it('should warn on invalid prop types', () => {
    expect(() =>
      createReactClass({
        displayName: 'Component',
        propTypes: {
          prop: null,
        },
        render: function () {
          return <span>{this.props.prop}</span>;
        },
      }),
    ).toErrorDev(
      'Component: prop type `prop` is invalid; ' +
        'it must be a function, usually from React.PropTypes.',
      {withoutStack: true},
    );
  });

  it('should warn on invalid context types', () => {
    expect(() =>
      createReactClass({
        displayName: 'Component',
        contextTypes: {
          prop: null,
        },
        render: function () {
          return <span>{this.props.prop}</span>;
        },
      }),
    ).toErrorDev(
      'Component: context type `prop` is invalid; ' +
        'it must be a function, usually from React.PropTypes.',
      {withoutStack: true},
    );
  });

  it('should throw on invalid child context types', () => {
    expect(() =>
      createReactClass({
        displayName: 'Component',
        childContextTypes: {
          prop: null,
        },
        render: function () {
          return <span>{this.props.prop}</span>;
        },
      }),
    ).toErrorDev(
      'Component: child context type `prop` is invalid; ' +
        'it must be a function, usually from React.PropTypes.',
      {withoutStack: true},
    );
  });

  it('should warn when misspelling shouldComponentUpdate', () => {
    expect(() =>
      createReactClass({
        componentShouldUpdate: function () {
          return false;
        },
        render: function () {
          return <div />;
        },
      }),
    ).toErrorDev(
      'A component has a method called componentShouldUpdate(). Did you ' +
        'mean shouldComponentUpdate()? The name is phrased as a question ' +
        'because the function is expected to return a value.',
      {withoutStack: true},
    );

    expect(() =>
      createReactClass({
        displayName: 'NamedComponent',
        componentShouldUpdate: function () {
          return false;
        },
        render: function () {
          return <div />;
        },
      }),
    ).toErrorDev(
      'NamedComponent has a method called componentShouldUpdate(). Did you ' +
        'mean shouldComponentUpdate()? The name is phrased as a question ' +
        'because the function is expected to return a value.',
      {withoutStack: true},
    );
  });

  it('should warn when misspelling componentWillReceiveProps', () => {
    expect(() =>
      createReactClass({
        componentWillRecieveProps: function () {
          return false;
        },
        render: function () {
          return <div />;
        },
      }),
    ).toErrorDev(
      'A component has a method called componentWillRecieveProps(). Did you ' +
        'mean componentWillReceiveProps()?',
      {withoutStack: true},
    );
  });

  it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => {
    expect(() =>
      createReactClass({
        UNSAFE_componentWillRecieveProps: function () {
          return false;
        },
        render: function () {
          return <div />;
        },
      }),
    ).toErrorDev(
      'A component has a method called UNSAFE_componentWillRecieveProps(). ' +
        'Did you mean UNSAFE_componentWillReceiveProps()?',
      {withoutStack: true},
    );
  });

  it('should throw if a reserved property is in statics', () => {
    expect(function () {
      createReactClass({
        statics: {
          getDefaultProps: function () {
            return {
              foo: 0,
            };
          },
        },

        render: function () {
          return <span />;
        },
      });
    }).toThrowError(
      'ReactClass: You are attempting to define a reserved property, ' +
        '`getDefaultProps`, that shouldn\'t be on the "statics" key. Define ' +
        'it as an instance property instead; it will still be accessible on ' +
        'the constructor.',
    );
  });

  // TODO: Consider actually moving these to statics or drop this unit test.
  // eslint-disable-next-line jest/no-disabled-tests
  it.skip('should warn when using deprecated non-static spec keys', () => {
    expect(() =>
      createReactClass({
        mixins: [{}],
        propTypes: {
          foo: PropTypes.string,
        },
        contextTypes: {
          foo: PropTypes.string,
        },
        childContextTypes: {
          foo: PropTypes.string,
        },
        render: function () {
          return <div />;
        },
      }),
    ).toErrorDev([
      '`mixins` is now a static property and should ' +
        'be defined inside "statics".',
      '`propTypes` is now a static property and should ' +
        'be defined inside "statics".',
      '`contextTypes` is now a static property and ' +
        'should be defined inside "statics".',
      '`childContextTypes` is now a static property and ' +
        'should be defined inside "statics".',
    ]);
  });

  it('should support statics', async () => {
    const Component = createReactClass({
      statics: {
        abc: 'def',
        def: 0,
        ghi: null,
        jkl: 'mno',
        pqr: function () {
          return this;
        },
      },

      render: function () {
        return <span />;
      },
    });
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    let instance;
    await act(() => {
      root.render(<Component ref={current => (instance = current)} />);
    });

    expect(instance.constructor.abc).toBe('def');
    expect(Component.abc).toBe('def');
    expect(instance.constructor.def).toBe(0);
    expect(Component.def).toBe(0);
    expect(instance.constructor.ghi).toBe(null);
    expect(Component.ghi).toBe(null);
    expect(instance.constructor.jkl).toBe('mno');
    expect(Component.jkl).toBe('mno');
    expect(instance.constructor.pqr()).toBe(Component);
    expect(Component.pqr()).toBe(Component);
  });

  it('should work with object getInitialState() return values', async () => {
    const Component = createReactClass({
      getInitialState: function () {
        return {
          occupation: 'clown',
        };
      },
      render: function () {
        return <span />;
      },
    });
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    let instance;
    await act(() => {
      root.render(<Component ref={current => (instance = current)} />);
    });

    expect(instance.state.occupation).toEqual('clown');
  });

  it('should work with getDerivedStateFromProps() return values', async () => {
    const Component = createReactClass({
      getInitialState() {
        return {};
      },
      render: function () {
        return <span />;
      },
    });
    Component.getDerivedStateFromProps = () => {
      return {occupation: 'clown'};
    };
    let instance;
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<Component ref={current => (instance = current)} />);
    });
    expect(instance.state.occupation).toEqual('clown');
  });

  // @gate !disableLegacyContext
  it('renders based on context getInitialState', async () => {
    const Foo = createReactClass({
      contextTypes: {
        className: PropTypes.string,
      },
      getInitialState() {
        return {className: this.context.className};
      },
      render() {
        return <span className={this.state.className} />;
      },
    });

    const Outer = createReactClass({
      childContextTypes: {
        className: PropTypes.string,
      },
      getChildContext() {
        return {className: 'foo'};
      },
      render() {
        return <Foo />;
      },
    });

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(<Outer />);
    });
    assertConsoleErrorDev([
      'Component uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
      'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
    ]);
    expect(container.firstChild.className).toBe('foo');
  });

  it('should throw with non-object getInitialState() return values', async () => {
    // eslint-disable-next-line no-for-of-loops/no-for-of-loops
    for (const state of [['an array'], 'a string', 1234]) {
      const Component = createReactClass({
        getInitialState: function () {
          return state;
        },
        render: function () {
          return <span />;
        },
      });
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await expect(
        act(() => {
          root.render(<Component />);
        }),
      ).rejects.toThrowError(
        'Component.getInitialState(): must return an object or null',
      );
    }
  });

  it('should work with a null getInitialState() return value', async () => {
    const Component = createReactClass({
      getInitialState: function () {
        return null;
      },
      render: function () {
        return <span />;
      },
    });
    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await expect(
      act(() => {
        root.render(<Component />);
      }),
    ).resolves.not.toThrow();
  });

  it('should throw when using legacy factories', () => {
    const Component = createReactClass({
      render() {
        return <div />;
      },
    });

    expect(() => expect(() => Component()).toThrow()).toErrorDev(
      'Something is calling a React component directly. Use a ' +
        'factory or JSX instead. See: https://fb.me/react-legacyfactory',
      {withoutStack: true},
    );
  });

  it('replaceState and callback works', async () => {
    const ops = [];
    const Component = createReactClass({
      getInitialState() {
        return {step: 0};
      },
      render() {
        ops.push('Render: ' + this.state.step);
        return <div />;
      },
    });

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    let instance;
    await act(() => {
      root.render(<Component ref={current => (instance = current)} />);
    });

    await act(() => {
      instance.replaceState({step: 1}, () => {
        ops.push('Callback: ' + instance.state.step);
      });
    });

    expect(ops).toEqual(['Render: 0', 'Render: 1', 'Callback: 1']);
  });

  it('getDerivedStateFromProps updates state when props change', async () => {
    const Component = createReactClass({
      getInitialState() {
        return {
          count: 1,
        };
      },
      render() {
        return <div>count:{this.state.count}</div>;
      },
    });
    Component.getDerivedStateFromProps = (nextProps, prevState) => ({
      count: prevState.count + nextProps.incrementBy,
    });

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await act(() => {
      root.render(
        <div>
          <Component incrementBy={0} />
        </div>,
      );
    });
    expect(container.firstChild.textContent).toEqual('count:1');
    await act(() => {
      root.render(
        <div>
          <Component incrementBy={2} />
        </div>,
      );
    });
    expect(container.firstChild.textContent).toEqual('count:3');
  });

  it('should support the new static getDerivedStateFromProps method', async () => {
    let instance;
    const Component = createReactClass({
      statics: {
        getDerivedStateFromProps: function () {
          return {foo: 'bar'};
        },
      },

      getInitialState() {
        return {};
      },

      render: function () {
        instance = this;
        return null;
      },
    });
    const root = ReactDOMClient.createRoot(document.createElement('div'));
    await act(() => {
      root.render(<Component />);
    });
    expect(instance.state.foo).toBe('bar');
  });

  it('warns if getDerivedStateFromProps is not static', async () => {
    const Foo = createReactClass({
      displayName: 'Foo',
      getDerivedStateFromProps() {
        return {};
      },
      render() {
        return <div />;
      },
    });
    await expect(async () => {
      const root = ReactDOMClient.createRoot(document.createElement('div'));
      await act(() => {
        root.render(<Foo foo="foo" />);
      });
    }).toErrorDev(
      'Foo: getDerivedStateFromProps() is defined as an instance method ' +
        'and will be ignored. Instead, declare it as a static method.',
    );
  });

  it('warns if getDerivedStateFromError is not static', async () => {
    const Foo = createReactClass({
      displayName: 'Foo',
      getDerivedStateFromError() {
        return {};
      },
      render() {
        return <div />;
      },
    });
    await expect(async () => {
      const root = ReactDOMClient.createRoot(document.createElement('div'));
      await act(() => {
        root.render(<Foo foo="foo" />);
      });
    }).toErrorDev(
      'Foo: getDerivedStateFromError() is defined as an instance method ' +
        'and will be ignored. Instead, declare it as a static method.',
    );
  });

  it('warns if getSnapshotBeforeUpdate is static', async () => {
    const Foo = createReactClass({
      displayName: 'Foo',
      statics: {
        getSnapshotBeforeUpdate: function () {
          return null;
        },
      },
      render() {
        return <div />;
      },
    });
    await expect(async () => {
      const root = ReactDOMClient.createRoot(document.createElement('div'));
      await act(() => {
        root.render(<Foo foo="foo" />);
      });
    }).toErrorDev(
      'Foo: getSnapshotBeforeUpdate() is defined as a static method ' +
        'and will be ignored. Instead, declare it as an instance method.',
    );
  });

  it('should warn if state is not properly initialized before getDerivedStateFromProps', async () => {
    const Component = createReactClass({
      displayName: 'Component',
      statics: {
        getDerivedStateFromProps: function () {
          return null;
        },
      },
      render: function () {
        return null;
      },
    });
    await expect(async () => {
      const root = ReactDOMClient.createRoot(document.createElement('div'));
      await act(() => {
        root.render(<Component />);
      });
    }).toErrorDev(
      '`Component` uses `getDerivedStateFromProps` but its initial state is ' +
        'null. This is not recommended. Instead, define the initial state by ' +
        'assigning an object to `this.state` in the constructor of `Component`. ' +
        'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.',
    );
  });

  it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', async () => {
    const Component = createReactClass({
      statics: {
        getDerivedStateFromProps: function () {
          return null;
        },
      },
      componentWillMount: function () {
        throw Error('unexpected');
      },
      componentWillReceiveProps: function () {
        throw Error('unexpected');
      },
      componentWillUpdate: function () {
        throw Error('unexpected');
      },
      getInitialState: function () {
        return {};
      },
      render: function () {
        return null;
      },
    });
    Component.displayName = 'Component';

    await expect(async () => {
      await expect(async () => {
        const root = ReactDOMClient.createRoot(document.createElement('div'));
        await act(() => {
          root.render(<Component />);
        });
      }).toErrorDev(
        'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
          'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
          '  componentWillMount\n' +
          '  componentWillReceiveProps\n' +
          '  componentWillUpdate\n\n' +
          'The above lifecycles should be removed. Learn more about this warning here:\n' +
          'https://react.dev/link/unsafe-component-lifecycles',
      );
    }).toWarnDev(
      [
        'componentWillMount has been renamed',
        'componentWillReceiveProps has been renamed',
        'componentWillUpdate has been renamed',
      ],
      {withoutStack: true},
    );
    const root = ReactDOMClient.createRoot(document.createElement('div'));
    await act(() => {
      root.render(<Component foo={1} />);
    });
  });

  it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', async () => {
    const Component = createReactClass({
      getSnapshotBeforeUpdate: function () {
        return null;
      },
      componentWillMount: function () {
        throw Error('unexpected');
      },
      componentWillReceiveProps: function () {
        throw Error('unexpected');
      },
      componentWillUpdate: function () {
        throw Error('unexpected');
      },
      componentDidUpdate: function () {},
      render: function () {
        return null;
      },
    });
    Component.displayName = 'Component';

    await expect(async () => {
      await expect(async () => {
        const root = ReactDOMClient.createRoot(document.createElement('div'));
        await act(() => {
          root.render(<Component />);
        });
      }).toErrorDev(
        'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
          'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
          '  componentWillMount\n' +
          '  componentWillReceiveProps\n' +
          '  componentWillUpdate\n\n' +
          'The above lifecycles should be removed. Learn more about this warning here:\n' +
          'https://react.dev/link/unsafe-component-lifecycles',
      );
    }).toWarnDev(
      [
        'componentWillMount has been renamed',
        'componentWillReceiveProps has been renamed',
        'componentWillUpdate has been renamed',
      ],
      {withoutStack: true},
    );
    await act(() => {
      const root2 = ReactDOMClient.createRoot(document.createElement('div'));
      root2.render(<Component foo={1} />);
    });
  });

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

    const Component = createReactClass({
      mixins: [
        {
          componentWillMount: function () {
            log.push('componentWillMount');
          },
          componentWillReceiveProps: function () {
            log.push('componentWillReceiveProps');
          },
          componentWillUpdate: function () {
            log.push('componentWillUpdate');
          },
        },
      ],
      UNSAFE_componentWillMount: function () {
        log.push('UNSAFE_componentWillMount');
      },
      UNSAFE_componentWillReceiveProps: function () {
        log.push('UNSAFE_componentWillReceiveProps');
      },
      UNSAFE_componentWillUpdate: function () {
        log.push('UNSAFE_componentWillUpdate');
      },
      render: function () {
        return null;
      },
    });

    const root = ReactDOMClient.createRoot(document.createElement('div'));

    await expect(async () => {
      await act(() => {
        root.render(<Component foo="bar" />);
      });
    }).toWarnDev(
      [
        'componentWillMount has been renamed',
        'componentWillReceiveProps has been renamed',
        'componentWillUpdate has been renamed',
      ],
      {withoutStack: true},
    );
    expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']);

    log.length = 0;

    await act(() => {
      root.render(<Component foo="baz" />);
    });
    expect(log).toEqual([
      'componentWillReceiveProps',
      'UNSAFE_componentWillReceiveProps',
      'componentWillUpdate',
      'UNSAFE_componentWillUpdate',
    ]);
  });

  it('isMounted works', async () => {
    const ops = [];
    let instance;
    const Component = createReactClass({
      displayName: 'MyComponent',
      mixins: [
        {
          UNSAFE_componentWillMount() {
            this.log('mixin.componentWillMount');
          },
          componentDidMount() {
            this.log('mixin.componentDidMount');
          },
          UNSAFE_componentWillUpdate() {
            this.log('mixin.componentWillUpdate');
          },
          componentDidUpdate() {
            this.log('mixin.componentDidUpdate');
          },
          componentWillUnmount() {
            this.log('mixin.componentWillUnmount');
          },
        },
      ],
      log(name) {
        ops.push(`${name}: ${this.isMounted()}`);
      },
      getInitialState() {
        this.log('getInitialState');
        return {};
      },
      UNSAFE_componentWillMount() {
        this.log('componentWillMount');
      },
      componentDidMount() {
        this.log('componentDidMount');
      },
      UNSAFE_componentWillUpdate() {
        this.log('componentWillUpdate');
      },
      componentDidUpdate() {
        this.log('componentDidUpdate');
      },
      componentWillUnmount() {
        this.log('componentWillUnmount');
      },
      render() {
        instance = this;
        this.log('render');
        return <div />;
      },
    });

    const root = ReactDOMClient.createRoot(document.createElement('div'));

    await expect(async () => {
      await act(() => {
        root.render(<Component />);
      });
    }).toErrorDev(
      'MyComponent: isMounted is deprecated. Instead, make sure to ' +
        'clean up subscriptions and pending requests in componentWillUnmount ' +
        'to prevent memory leaks.',
      // This now has a component stack even though it's part of a third-party library.
    );

    // Dedupe

    await act(() => {
      root.render(<Component />);
    });

    await act(() => {
      root.unmount();
    });
    instance.log('after unmount');
    expect(ops).toEqual([
      'getInitialState: false',
      'mixin.componentWillMount: false',
      'componentWillMount: false',
      'render: false',
      'mixin.componentDidMount: true',
      'componentDidMount: true',
      'mixin.componentWillUpdate: true',
      'componentWillUpdate: true',
      'render: true',
      'mixin.componentDidUpdate: true',
      'componentDidUpdate: true',
      'mixin.componentWillUnmount: true',
      'componentWillUnmount: true',
      'after unmount: false',
    ]);
  });
});
