/**
 * 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
 * @jest-environment node
 */

'use strict';

let React;
let ReactDebugTools;

describe('ReactHooksInspection', () => {
  beforeEach(() => {
    jest.resetModules();
    React = require('react');
    ReactDebugTools = require('react-debug-tools');
  });

  it('should inspect a simple useState hook', () => {
    function Foo(props) {
      const [state] = React.useState('hello world');
      return <div>{state}</div>;
    }
    const tree = ReactDebugTools.inspectHooks(Foo, {});
    expect(tree).toEqual([
      {
        isStateEditable: true,
        id: 0,
        name: 'State',
        value: 'hello world',
        subHooks: [],
      },
    ]);
  });

  it('should inspect a simple custom hook', () => {
    function useCustom(value) {
      const [state] = React.useState(value);
      React.useDebugValue('custom hook label');
      return state;
    }
    function Foo(props) {
      const value = useCustom('hello world');
      return <div>{value}</div>;
    }
    const tree = ReactDebugTools.inspectHooks(Foo, {});
    expect(tree).toEqual([
      {
        isStateEditable: false,
        id: null,
        name: 'Custom',
        value: __DEV__ ? 'custom hook label' : undefined,
        subHooks: [
          {
            isStateEditable: true,
            id: 0,
            name: 'State',
            value: 'hello world',
            subHooks: [],
          },
        ],
      },
    ]);
  });

  it('should inspect a tree of multiple hooks', () => {
    function effect() {}
    function useCustom(value) {
      const [state] = React.useState(value);
      React.useEffect(effect);
      return state;
    }
    function Foo(props) {
      const value1 = useCustom('hello');
      const value2 = useCustom('world');
      return (
        <div>
          {value1} {value2}
        </div>
      );
    }
    const tree = ReactDebugTools.inspectHooks(Foo, {});
    expect(tree).toEqual([
      {
        isStateEditable: false,
        id: null,
        name: 'Custom',
        value: undefined,
        subHooks: [
          {
            isStateEditable: true,
            id: 0,
            name: 'State',
            subHooks: [],
            value: 'hello',
          },
          {
            isStateEditable: false,
            id: 1,
            name: 'Effect',
            subHooks: [],
            value: effect,
          },
        ],
      },
      {
        isStateEditable: false,
        id: null,
        name: 'Custom',
        value: undefined,
        subHooks: [
          {
            isStateEditable: true,
            id: 2,
            name: 'State',
            value: 'world',
            subHooks: [],
          },
          {
            isStateEditable: false,
            id: 3,
            name: 'Effect',
            value: effect,
            subHooks: [],
          },
        ],
      },
    ]);
  });

  it('should inspect a tree of multiple levels of hooks', () => {
    function effect() {}
    function useCustom(value) {
      const [state] = React.useReducer((s, a) => s, value);
      React.useEffect(effect);
      return state;
    }
    function useBar(value) {
      const result = useCustom(value);
      React.useLayoutEffect(effect);
      return result;
    }
    function useBaz(value) {
      React.useLayoutEffect(effect);
      const result = useCustom(value);
      return result;
    }
    function Foo(props) {
      const value1 = useBar('hello');
      const value2 = useBaz('world');
      return (
        <div>
          {value1} {value2}
        </div>
      );
    }
    const tree = ReactDebugTools.inspectHooks(Foo, {});
    expect(tree).toEqual([
      {
        isStateEditable: false,
        id: null,
        name: 'Bar',
        value: undefined,
        subHooks: [
          {
            isStateEditable: false,
            id: null,
            name: 'Custom',
            value: undefined,
            subHooks: [
              {
                isStateEditable: true,
                id: 0,
                name: 'Reducer',
                value: 'hello',
                subHooks: [],
              },
              {
                isStateEditable: false,
                id: 1,
                name: 'Effect',
                value: effect,
                subHooks: [],
              },
            ],
          },
          {
            isStateEditable: false,
            id: 2,
            name: 'LayoutEffect',
            value: effect,
            subHooks: [],
          },
        ],
      },
      {
        isStateEditable: false,
        id: null,
        name: 'Baz',
        value: undefined,
        subHooks: [
          {
            isStateEditable: false,
            id: 3,
            name: 'LayoutEffect',
            value: effect,
            subHooks: [],
          },
          {
            isStateEditable: false,
            id: null,
            name: 'Custom',
            subHooks: [
              {
                isStateEditable: true,
                id: 4,
                name: 'Reducer',
                subHooks: [],
                value: 'world',
              },
              {
                isStateEditable: false,
                id: 5,
                name: 'Effect',
                subHooks: [],
                value: effect,
              },
            ],
            value: undefined,
          },
        ],
      },
    ]);
  });

  it('should inspect the default value using the useContext hook', () => {
    const MyContext = React.createContext('default');
    function Foo(props) {
      const value = React.useContext(MyContext);
      return <div>{value}</div>;
    }
    const tree = ReactDebugTools.inspectHooks(Foo, {});
    expect(tree).toEqual([
      {
        isStateEditable: false,
        id: null,
        name: 'Context',
        value: 'default',
        subHooks: [],
      },
    ]);
  });

  it('should support an injected dispatcher', () => {
    const initial = {
      useState() {
        throw new Error("Should've been proxied");
      },
    };
    let current = initial;
    let getterCalls = 0;
    const setterCalls = [];
    const FakeDispatcherRef = {
      get current() {
        getterCalls++;
        return current;
      },
      set current(value) {
        setterCalls.push(value);
        current = value;
      },
    };

    function Foo(props) {
      const [state] = FakeDispatcherRef.current.useState('hello world');
      return <div>{state}</div>;
    }

    ReactDebugTools.inspectHooks(Foo, {}, FakeDispatcherRef);

    expect(getterCalls).toBe(2);
    expect(setterCalls).toHaveLength(2);
    expect(setterCalls[0]).not.toBe(initial);
    expect(setterCalls[1]).toBe(initial);
  });

  describe('useDebugValue', () => {
    it('should be ignored when called outside of a custom hook', () => {
      function Foo(props) {
        React.useDebugValue('this is invalid');
        return null;
      }
      const tree = ReactDebugTools.inspectHooks(Foo, {});
      expect(tree).toHaveLength(0);
    });

    it('should support an optional formatter function param', () => {
      function useCustom() {
        React.useDebugValue({bar: 123}, object => `bar:${object.bar}`);
        React.useState(0);
      }
      function Foo(props) {
        useCustom();
        return null;
      }
      const tree = ReactDebugTools.inspectHooks(Foo, {});
      expect(tree).toEqual([
        {
          isStateEditable: false,
          id: null,
          name: 'Custom',
          value: __DEV__ ? 'bar:123' : undefined,
          subHooks: [
            {
              isStateEditable: true,
              id: 0,
              name: 'State',
              subHooks: [],
              value: 0,
            },
          ],
        },
      ]);
    });
  });
});
