/**
 * 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 React;
let ReactTestRenderer;
let Scheduler;
let act;
let assertLog;

describe('StrictEffectsMode', () => {
  beforeEach(() => {
    jest.resetModules();
    React = require('react');
    ReactTestRenderer = require('react-test-renderer');
    Scheduler = require('scheduler');
    act = require('internal-test-utils').act;

    const InternalTestUtils = require('internal-test-utils');
    assertLog = InternalTestUtils.assertLog;
  });

  function supportsDoubleInvokeEffects() {
    return gate(
      flags =>
        flags.build === 'development' &&
        flags.createRootStrictEffectsByDefault &&
        flags.dfsEffectsRefactor,
    );
  }

  it('should not double invoke effects in legacy mode', async () => {
    function App({text}) {
      React.useEffect(() => {
        Scheduler.log('useEffect mount');
        return () => Scheduler.log('useEffect unmount');
      });

      React.useLayoutEffect(() => {
        Scheduler.log('useLayoutEffect mount');
        return () => Scheduler.log('useLayoutEffect unmount');
      });

      return text;
    }

    await act(() => {
      ReactTestRenderer.create(<App text={'mount'} />);
    });

    assertLog(['useLayoutEffect mount', 'useEffect mount']);
  });

  it('double invoking for effects works properly', async () => {
    function App({text}) {
      React.useEffect(() => {
        Scheduler.log('useEffect mount');
        return () => Scheduler.log('useEffect unmount');
      });

      React.useLayoutEffect(() => {
        Scheduler.log('useLayoutEffect mount');
        return () => Scheduler.log('useLayoutEffect unmount');
      });

      return text;
    }

    let renderer;
    await act(() => {
      renderer = ReactTestRenderer.create(<App text={'mount'} />, {
        isConcurrent: true,
      });
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'useLayoutEffect mount',
        'useEffect mount',
        'useLayoutEffect unmount',
        'useEffect unmount',
        'useLayoutEffect mount',
        'useEffect mount',
      ]);
    } else {
      assertLog(['useLayoutEffect mount', 'useEffect mount']);
    }

    await act(() => {
      renderer.update(<App text={'update'} />);
    });

    assertLog([
      'useLayoutEffect unmount',
      'useLayoutEffect mount',
      'useEffect unmount',
      'useEffect mount',
    ]);

    await act(() => {
      renderer.unmount();
    });

    assertLog(['useLayoutEffect unmount', 'useEffect unmount']);
  });

  it('multiple effects are double invoked in the right order (all mounted, all unmounted, all remounted)', async () => {
    function App({text}) {
      React.useEffect(() => {
        Scheduler.log('useEffect One mount');
        return () => Scheduler.log('useEffect One unmount');
      });

      React.useEffect(() => {
        Scheduler.log('useEffect Two mount');
        return () => Scheduler.log('useEffect Two unmount');
      });

      return text;
    }

    let renderer;
    await act(() => {
      renderer = ReactTestRenderer.create(<App text={'mount'} />, {
        isConcurrent: true,
      });
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'useEffect One mount',
        'useEffect Two mount',
        'useEffect One unmount',
        'useEffect Two unmount',
        'useEffect One mount',
        'useEffect Two mount',
      ]);
    } else {
      assertLog(['useEffect One mount', 'useEffect Two mount']);
    }

    await act(() => {
      renderer.update(<App text={'update'} />);
    });

    assertLog([
      'useEffect One unmount',
      'useEffect Two unmount',
      'useEffect One mount',
      'useEffect Two mount',
    ]);

    await act(() => {
      renderer.unmount(null);
    });

    assertLog(['useEffect One unmount', 'useEffect Two unmount']);
  });

  it('multiple layout effects are double invoked in the right order (all mounted, all unmounted, all remounted)', async () => {
    function App({text}) {
      React.useLayoutEffect(() => {
        Scheduler.log('useLayoutEffect One mount');
        return () => Scheduler.log('useLayoutEffect One unmount');
      });

      React.useLayoutEffect(() => {
        Scheduler.log('useLayoutEffect Two mount');
        return () => Scheduler.log('useLayoutEffect Two unmount');
      });

      return text;
    }

    let renderer;
    await act(() => {
      renderer = ReactTestRenderer.create(<App text={'mount'} />, {
        isConcurrent: true,
      });
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'useLayoutEffect One mount',
        'useLayoutEffect Two mount',
        'useLayoutEffect One unmount',
        'useLayoutEffect Two unmount',
        'useLayoutEffect One mount',
        'useLayoutEffect Two mount',
      ]);
    } else {
      assertLog(['useLayoutEffect One mount', 'useLayoutEffect Two mount']);
    }

    await act(() => {
      renderer.update(<App text={'update'} />);
    });

    assertLog([
      'useLayoutEffect One unmount',
      'useLayoutEffect Two unmount',
      'useLayoutEffect One mount',
      'useLayoutEffect Two mount',
    ]);

    await act(() => {
      renderer.unmount();
    });

    assertLog(['useLayoutEffect One unmount', 'useLayoutEffect Two unmount']);
  });

  it('useEffect and useLayoutEffect is called twice when there is no unmount', async () => {
    function App({text}) {
      React.useEffect(() => {
        Scheduler.log('useEffect mount');
      });

      React.useLayoutEffect(() => {
        Scheduler.log('useLayoutEffect mount');
      });

      return text;
    }

    let renderer;
    await act(() => {
      renderer = ReactTestRenderer.create(<App text={'mount'} />, {
        isConcurrent: true,
      });
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'useLayoutEffect mount',
        'useEffect mount',
        'useLayoutEffect mount',
        'useEffect mount',
      ]);
    } else {
      assertLog(['useLayoutEffect mount', 'useEffect mount']);
    }

    await act(() => {
      renderer.update(<App text={'update'} />);
    });

    assertLog(['useLayoutEffect mount', 'useEffect mount']);

    await act(() => {
      renderer.unmount();
    });

    assertLog([]);
  });

  it('passes the right context to class component lifecycles', async () => {
    class App extends React.PureComponent {
      test() {}

      componentDidMount() {
        this.test();
        Scheduler.log('componentDidMount');
      }

      componentDidUpdate() {
        this.test();
        Scheduler.log('componentDidUpdate');
      }

      componentWillUnmount() {
        this.test();
        Scheduler.log('componentWillUnmount');
      }

      render() {
        return null;
      }
    }

    await act(() => {
      ReactTestRenderer.create(<App />, {isConcurrent: true});
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'componentDidMount',
        'componentWillUnmount',
        'componentDidMount',
      ]);
    } else {
      assertLog(['componentDidMount']);
    }
  });

  it('double invoking works for class components', async () => {
    class App extends React.PureComponent {
      componentDidMount() {
        Scheduler.log('componentDidMount');
      }

      componentDidUpdate() {
        Scheduler.log('componentDidUpdate');
      }

      componentWillUnmount() {
        Scheduler.log('componentWillUnmount');
      }

      render() {
        return this.props.text;
      }
    }

    let renderer;
    await act(() => {
      renderer = ReactTestRenderer.create(<App text={'mount'} />, {
        isConcurrent: true,
      });
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'componentDidMount',
        'componentWillUnmount',
        'componentDidMount',
      ]);
    } else {
      assertLog(['componentDidMount']);
    }

    await act(() => {
      renderer.update(<App text={'update'} />);
    });

    assertLog(['componentDidUpdate']);

    await act(() => {
      renderer.unmount();
    });

    assertLog(['componentWillUnmount']);
  });

  it('invokes componentWillUnmount for class components without componentDidMount', async () => {
    class App extends React.PureComponent {
      componentDidUpdate() {
        Scheduler.log('componentDidUpdate');
      }

      componentWillUnmount() {
        Scheduler.log('componentWillUnmount');
      }

      render() {
        return this.props.text;
      }
    }

    let renderer;
    await act(() => {
      renderer = ReactTestRenderer.create(<App text={'mount'} />, {
        isConcurrent: true,
      });
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog(['componentWillUnmount']);
    } else {
      assertLog([]);
    }

    await act(() => {
      renderer.update(<App text={'update'} />);
    });

    assertLog(['componentDidUpdate']);

    await act(() => {
      renderer.unmount();
    });

    assertLog(['componentWillUnmount']);
  });

  it('should not double invoke class lifecycles in legacy mode', async () => {
    class App extends React.PureComponent {
      componentDidMount() {
        Scheduler.log('componentDidMount');
      }

      componentDidUpdate() {
        Scheduler.log('componentDidUpdate');
      }

      componentWillUnmount() {
        Scheduler.log('componentWillUnmount');
      }

      render() {
        return this.props.text;
      }
    }

    await act(() => {
      ReactTestRenderer.create(<App text={'mount'} />);
    });

    assertLog(['componentDidMount']);
  });

  it('double flushing passive effects only results in one double invoke', async () => {
    function App({text}) {
      const [state, setState] = React.useState(0);
      React.useEffect(() => {
        if (state !== 1) {
          setState(1);
        }
        Scheduler.log('useEffect mount');
        return () => Scheduler.log('useEffect unmount');
      });

      React.useLayoutEffect(() => {
        Scheduler.log('useLayoutEffect mount');
        return () => Scheduler.log('useLayoutEffect unmount');
      });

      Scheduler.log(text);
      return text;
    }

    await act(() => {
      ReactTestRenderer.create(<App text={'mount'} />, {
        isConcurrent: true,
      });
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'mount',
        'useLayoutEffect mount',
        'useEffect mount',
        'useLayoutEffect unmount',
        'useEffect unmount',
        'useLayoutEffect mount',
        'useEffect mount',
        'mount',
        'useLayoutEffect unmount',
        'useLayoutEffect mount',
        'useEffect unmount',
        'useEffect mount',
      ]);
    } else {
      assertLog([
        'mount',
        'useLayoutEffect mount',
        'useEffect mount',
        'mount',
        'useLayoutEffect unmount',
        'useLayoutEffect mount',
        'useEffect unmount',
        'useEffect mount',
      ]);
    }
  });

  it('newly mounted components after initial mount get double invoked', async () => {
    let _setShowChild;
    function Child() {
      React.useEffect(() => {
        Scheduler.log('Child useEffect mount');
        return () => Scheduler.log('Child useEffect unmount');
      });
      React.useLayoutEffect(() => {
        Scheduler.log('Child useLayoutEffect mount');
        return () => Scheduler.log('Child useLayoutEffect unmount');
      });

      return null;
    }

    function App() {
      const [showChild, setShowChild] = React.useState(false);
      _setShowChild = setShowChild;
      React.useEffect(() => {
        Scheduler.log('App useEffect mount');
        return () => Scheduler.log('App useEffect unmount');
      });
      React.useLayoutEffect(() => {
        Scheduler.log('App useLayoutEffect mount');
        return () => Scheduler.log('App useLayoutEffect unmount');
      });

      return showChild && <Child />;
    }

    await act(() => {
      ReactTestRenderer.create(<App />, {isConcurrent: true});
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'App useLayoutEffect mount',
        'App useEffect mount',
        'App useLayoutEffect unmount',
        'App useEffect unmount',
        'App useLayoutEffect mount',
        'App useEffect mount',
      ]);
    } else {
      assertLog(['App useLayoutEffect mount', 'App useEffect mount']);
    }

    await act(() => {
      _setShowChild(true);
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'App useLayoutEffect unmount',
        'Child useLayoutEffect mount',
        'App useLayoutEffect mount',
        'App useEffect unmount',
        'Child useEffect mount',
        'App useEffect mount',
        'Child useLayoutEffect unmount',
        'Child useEffect unmount',
        'Child useLayoutEffect mount',
        'Child useEffect mount',
      ]);
    } else {
      assertLog([
        'App useLayoutEffect unmount',
        'Child useLayoutEffect mount',
        'App useLayoutEffect mount',
        'App useEffect unmount',
        'Child useEffect mount',
        'App useEffect mount',
      ]);
    }
  });

  it('classes and functions are double invoked together correctly', async () => {
    class ClassChild extends React.PureComponent {
      componentDidMount() {
        Scheduler.log('componentDidMount');
      }

      componentWillUnmount() {
        Scheduler.log('componentWillUnmount');
      }

      render() {
        return this.props.text;
      }
    }

    function FunctionChild({text}) {
      React.useEffect(() => {
        Scheduler.log('useEffect mount');
        return () => Scheduler.log('useEffect unmount');
      });
      React.useLayoutEffect(() => {
        Scheduler.log('useLayoutEffect mount');
        return () => Scheduler.log('useLayoutEffect unmount');
      });
      return text;
    }

    function App({text}) {
      return (
        <>
          <ClassChild text={text} />
          <FunctionChild text={text} />
        </>
      );
    }

    let renderer;
    await act(() => {
      renderer = ReactTestRenderer.create(<App text={'mount'} />, {
        isConcurrent: true,
      });
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'componentDidMount',
        'useLayoutEffect mount',
        'useEffect mount',
        'componentWillUnmount',
        'useLayoutEffect unmount',
        'useEffect unmount',
        'componentDidMount',
        'useLayoutEffect mount',
        'useEffect mount',
      ]);
    } else {
      assertLog([
        'componentDidMount',
        'useLayoutEffect mount',
        'useEffect mount',
      ]);
    }

    await act(() => {
      renderer.update(<App text={'mount'} />);
    });

    assertLog([
      'useLayoutEffect unmount',
      'useLayoutEffect mount',
      'useEffect unmount',
      'useEffect mount',
    ]);

    await act(() => {
      renderer.unmount();
    });

    assertLog([
      'componentWillUnmount',
      'useLayoutEffect unmount',
      'useEffect unmount',
    ]);
  });

  it('classes without componentDidMount and functions are double invoked together correctly', async () => {
    class ClassChild extends React.PureComponent {
      componentWillUnmount() {
        Scheduler.log('componentWillUnmount');
      }

      render() {
        return this.props.text;
      }
    }

    function FunctionChild({text}) {
      React.useEffect(() => {
        Scheduler.log('useEffect mount');
        return () => Scheduler.log('useEffect unmount');
      });
      React.useLayoutEffect(() => {
        Scheduler.log('useLayoutEffect mount');
        return () => Scheduler.log('useLayoutEffect unmount');
      });
      return text;
    }

    function App({text}) {
      return (
        <>
          <ClassChild text={text} />
          <FunctionChild text={text} />
        </>
      );
    }

    let renderer;
    await act(() => {
      renderer = ReactTestRenderer.create(<App text={'mount'} />, {
        isConcurrent: true,
      });
    });

    if (supportsDoubleInvokeEffects()) {
      assertLog([
        'useLayoutEffect mount',
        'useEffect mount',
        'componentWillUnmount',
        'useLayoutEffect unmount',
        'useEffect unmount',
        'useLayoutEffect mount',
        'useEffect mount',
      ]);
    } else {
      assertLog(['useLayoutEffect mount', 'useEffect mount']);
    }

    await act(() => {
      renderer.update(<App text={'mount'} />);
    });

    assertLog([
      'useLayoutEffect unmount',
      'useLayoutEffect mount',
      'useEffect unmount',
      'useEffect mount',
    ]);

    await act(() => {
      renderer.unmount();
    });

    assertLog([
      'componentWillUnmount',
      'useLayoutEffect unmount',
      'useEffect unmount',
    ]);
  });
});
