/**
 * 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 PropTypes;
let React;
let ReactDOM;
let ReactTestUtils;

function FunctionComponent(props) {
  return <div>{props.name}</div>;
}

describe('ReactFunctionComponent', () => {
  beforeEach(() => {
    jest.resetModules();
    PropTypes = require('prop-types');
    React = require('react');
    ReactDOM = require('react-dom');
    ReactTestUtils = require('react-dom/test-utils');
  });

  it('should render stateless component', () => {
    const el = document.createElement('div');
    ReactDOM.render(<FunctionComponent name="A" />, el);

    expect(el.textContent).toBe('A');
  });

  it('should update stateless component', () => {
    class Parent extends React.Component {
      render() {
        return <FunctionComponent {...this.props} />;
      }
    }

    const el = document.createElement('div');
    ReactDOM.render(<Parent name="A" />, el);
    expect(el.textContent).toBe('A');

    ReactDOM.render(<Parent name="B" />, el);
    expect(el.textContent).toBe('B');
  });

  it('should unmount stateless component', () => {
    const container = document.createElement('div');

    ReactDOM.render(<FunctionComponent name="A" />, container);
    expect(container.textContent).toBe('A');

    ReactDOM.unmountComponentAtNode(container);
    expect(container.textContent).toBe('');
  });

  // @gate !disableLegacyContext
  it('should pass context thru stateless component', () => {
    class Child extends React.Component {
      static contextTypes = {
        test: PropTypes.string.isRequired,
      };

      render() {
        return <div>{this.context.test}</div>;
      }
    }

    function Parent() {
      return <Child />;
    }

    class GrandParent extends React.Component {
      static childContextTypes = {
        test: PropTypes.string.isRequired,
      };

      getChildContext() {
        return {test: this.props.test};
      }

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

    const el = document.createElement('div');
    ReactDOM.render(<GrandParent test="test" />, el);

    expect(el.textContent).toBe('test');

    ReactDOM.render(<GrandParent test="mest" />, el);

    expect(el.textContent).toBe('mest');
  });

  it('should warn for getDerivedStateFromProps on a function component', () => {
    function FunctionComponentWithChildContext() {
      return null;
    }
    FunctionComponentWithChildContext.getDerivedStateFromProps = function () {};

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

    expect(() =>
      ReactDOM.render(<FunctionComponentWithChildContext />, container),
    ).toErrorDev(
      'FunctionComponentWithChildContext: Function ' +
        'components do not support getDerivedStateFromProps.',
    );
  });

  it('should warn for childContextTypes on a function component', () => {
    function FunctionComponentWithChildContext(props) {
      return <div>{props.name}</div>;
    }

    FunctionComponentWithChildContext.childContextTypes = {
      foo: PropTypes.string,
    };

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

    expect(() =>
      ReactDOM.render(
        <FunctionComponentWithChildContext name="A" />,
        container,
      ),
    ).toErrorDev(
      'FunctionComponentWithChildContext(...): childContextTypes cannot ' +
        'be defined on a function component.',
    );
  });

  it('should not throw when stateless component returns undefined', () => {
    function NotAComponent() {}
    expect(function () {
      ReactTestUtils.renderIntoDocument(
        <div>
          <NotAComponent />
        </div>,
      );
    }).not.toThrowError();
  });

  it('should throw on string refs in pure functions', () => {
    function Child() {
      return <div ref="me" />;
    }

    expect(function () {
      ReactTestUtils.renderIntoDocument(<Child test="test" />);
    }).toThrowError(
      __DEV__
        ? 'Function components cannot have string refs. We recommend using useRef() instead.'
        : // It happens because we don't save _owner in production for
          // function components.
          'Element ref was specified as a string (me) but no owner was set. This could happen for one of' +
            ' the following reasons:\n' +
            '1. You may be adding a ref to a function component\n' +
            "2. You may be adding a ref to a component that was not created inside a component's render method\n" +
            '3. You have multiple copies of React loaded\n' +
            'See https://reactjs.org/link/refs-must-have-owner for more information.',
    );
  });

  it('should warn when given a string ref', () => {
    function Indirection(props) {
      return <div>{props.children}</div>;
    }

    class ParentUsingStringRef extends React.Component {
      render() {
        return (
          <Indirection>
            <FunctionComponent name="A" ref="stateless" />
          </Indirection>
        );
      }
    }

    expect(() =>
      ReactTestUtils.renderIntoDocument(<ParentUsingStringRef />),
    ).toErrorDev(
      'Warning: Function components cannot be given refs. ' +
        'Attempts to access this ref will fail. ' +
        'Did you mean to use React.forwardRef()?\n\n' +
        'Check the render method ' +
        'of `ParentUsingStringRef`.\n' +
        '    in FunctionComponent (at **)\n' +
        '    in div (at **)\n' +
        '    in Indirection (at **)\n' +
        '    in ParentUsingStringRef (at **)',
    );

    // No additional warnings should be logged
    ReactTestUtils.renderIntoDocument(<ParentUsingStringRef />);
  });

  it('should warn when given a function ref', () => {
    function Indirection(props) {
      return <div>{props.children}</div>;
    }

    class ParentUsingFunctionRef extends React.Component {
      render() {
        return (
          <Indirection>
            <FunctionComponent
              name="A"
              ref={arg => {
                expect(arg).toBe(null);
              }}
            />
          </Indirection>
        );
      }
    }

    expect(() =>
      ReactTestUtils.renderIntoDocument(<ParentUsingFunctionRef />),
    ).toErrorDev(
      'Warning: Function components cannot be given refs. ' +
        'Attempts to access this ref will fail. ' +
        'Did you mean to use React.forwardRef()?\n\n' +
        'Check the render method ' +
        'of `ParentUsingFunctionRef`.\n' +
        '    in FunctionComponent (at **)\n' +
        '    in div (at **)\n' +
        '    in Indirection (at **)\n' +
        '    in ParentUsingFunctionRef (at **)',
    );

    // No additional warnings should be logged
    ReactTestUtils.renderIntoDocument(<ParentUsingFunctionRef />);
  });

  it('deduplicates ref warnings based on element or owner', () => {
    // When owner uses JSX, we can use exact line location to dedupe warnings
    class AnonymousParentUsingJSX extends React.Component {
      render() {
        return <FunctionComponent name="A" ref={() => {}} />;
      }
    }
    Object.defineProperty(AnonymousParentUsingJSX, 'name', {value: undefined});

    let instance1;

    expect(() => {
      instance1 = ReactTestUtils.renderIntoDocument(
        <AnonymousParentUsingJSX />,
      );
    }).toErrorDev('Warning: Function components cannot be given refs.');
    // Should be deduped (offending element is on the same line):
    instance1.forceUpdate();
    // Should also be deduped (offending element is on the same line):
    ReactTestUtils.renderIntoDocument(<AnonymousParentUsingJSX />);

    // When owner doesn't use JSX, and is anonymous, we warn once per internal instance.
    class AnonymousParentNotUsingJSX extends React.Component {
      render() {
        return React.createElement(FunctionComponent, {
          name: 'A',
          ref: () => {},
        });
      }
    }
    Object.defineProperty(AnonymousParentNotUsingJSX, 'name', {
      value: undefined,
    });

    let instance2;
    expect(() => {
      instance2 = ReactTestUtils.renderIntoDocument(
        <AnonymousParentNotUsingJSX />,
      );
    }).toErrorDev('Warning: Function components cannot be given refs.');
    // Should be deduped (same internal instance, no additional warnings)
    instance2.forceUpdate();
    // Could not be differentiated (since owner is anonymous and no source location)
    ReactTestUtils.renderIntoDocument(<AnonymousParentNotUsingJSX />);

    // When owner doesn't use JSX, but is named, we warn once per owner name
    class NamedParentNotUsingJSX extends React.Component {
      render() {
        return React.createElement(FunctionComponent, {
          name: 'A',
          ref: () => {},
        });
      }
    }
    let instance3;
    expect(() => {
      instance3 = ReactTestUtils.renderIntoDocument(<NamedParentNotUsingJSX />);
    }).toErrorDev('Warning: Function components cannot be given refs.');
    // Should be deduped (same owner name, no additional warnings):
    instance3.forceUpdate();
    // Should also be deduped (same owner name, no additional warnings):
    ReactTestUtils.renderIntoDocument(<NamedParentNotUsingJSX />);
  });

  // This guards against a regression caused by clearing the current debug fiber.
  // https://github.com/facebook/react/issues/10831
  // @gate !disableLegacyContext || !__DEV__
  it('should warn when giving a function ref with context', () => {
    function Child() {
      return null;
    }
    Child.contextTypes = {
      foo: PropTypes.string,
    };

    class Parent extends React.Component {
      static childContextTypes = {
        foo: PropTypes.string,
      };
      getChildContext() {
        return {
          foo: 'bar',
        };
      }
      render() {
        return <Child ref={function () {}} />;
      }
    }

    expect(() => ReactTestUtils.renderIntoDocument(<Parent />)).toErrorDev(
      'Warning: Function components cannot be given refs. ' +
        'Attempts to access this ref will fail. ' +
        'Did you mean to use React.forwardRef()?\n\n' +
        'Check the render method ' +
        'of `Parent`.\n' +
        '    in Child (at **)\n' +
        '    in Parent (at **)',
    );
  });

  it('should provide a null ref', () => {
    function Child() {
      return <div />;
    }

    const comp = ReactTestUtils.renderIntoDocument(<Child />);
    expect(comp).toBe(null);
  });

  it('should use correct name in key warning', () => {
    function Child() {
      return <div>{[<span />]}</div>;
    }

    expect(() => ReactTestUtils.renderIntoDocument(<Child />)).toErrorDev(
      'Each child in a list should have a unique "key" prop.\n\n' +
        'Check the render method of `Child`.',
    );
  });

  // TODO: change this test after we deprecate default props support
  // for function components
  it('should support default props and prop types', () => {
    function Child(props) {
      return <div>{props.test}</div>;
    }
    Child.defaultProps = {test: 2};
    Child.propTypes = {test: PropTypes.string};

    expect(() => ReactTestUtils.renderIntoDocument(<Child />)).toErrorDev([
      'Warning: Child: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.',
      'Warning: Failed prop type: Invalid prop `test` of type `number` ' +
        'supplied to `Child`, expected `string`.\n' +
        '    in Child (at **)',
    ]);
  });

  // @gate !disableLegacyContext
  it('should receive context', () => {
    class Parent extends React.Component {
      static childContextTypes = {
        lang: PropTypes.string,
      };

      getChildContext() {
        return {lang: 'en'};
      }

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

    function Child(props, context) {
      return <div>{context.lang}</div>;
    }
    Child.contextTypes = {lang: PropTypes.string};

    const el = document.createElement('div');
    ReactDOM.render(<Parent />, el);
    expect(el.textContent).toBe('en');
  });

  it('should work with arrow functions', () => {
    let Child = function () {
      return <div />;
    };
    // Will create a new bound function without a prototype, much like a native
    // arrow function.
    Child = Child.bind(this);

    expect(() => ReactTestUtils.renderIntoDocument(<Child />)).not.toThrow();
  });

  it('should allow simple functions to return null', () => {
    const Child = function () {
      return null;
    };
    expect(() => ReactTestUtils.renderIntoDocument(<Child />)).not.toThrow();
  });

  it('should allow simple functions to return false', () => {
    function Child() {
      return false;
    }
    expect(() => ReactTestUtils.renderIntoDocument(<Child />)).not.toThrow();
  });
});