/**
 * 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.
 *
 * @flow
 */

// Note that this test uses React components declared in the "__source__" directory.
// This is done to control if and how the code is transformed at runtime.
// Do not declare test components within this test file as it is very fragile.

function expectHookNamesToEqual(map, expectedNamesArray) {
  // Slightly hacky since it relies on the iterable order of values()
  expect(Array.from(map.values())).toEqual(expectedNamesArray);
}

function requireText(path, encoding) {
  const {existsSync, readFileSync} = require('fs');
  if (existsSync(path)) {
    return Promise.resolve(readFileSync(path, encoding));
  } else {
    return Promise.reject(`File not found "${path}"`);
  }
}

function initFetchMock() {
  const fetchMock = require('jest-fetch-mock');
  fetchMock.enableMocks();
  fetchMock.mockIf(/.+$/, request => {
    const url = request.url;
    const isLoadingExternalSourceMap = /external\/.*\.map/.test(url);
    if (isLoadingExternalSourceMap) {
      // Assert that url contains correct query params
      expect(url.includes('?foo=bar&param=some_value')).toBe(true);
      const fileSystemPath = url.split('?')[0];
      return requireText(fileSystemPath, 'utf8');
    }
    return requireText(url, 'utf8');
  });
  return fetchMock;
}

describe('parseHookNames', () => {
  let fetchMock;
  let inspectHooks;
  let parseHookNames;

  beforeEach(() => {
    jest.resetModules();

    jest.mock('source-map-support', () => {
      console.trace('source-map-support');
    });

    fetchMock = initFetchMock();

    inspectHooks =
      require('react-debug-tools/src/ReactDebugHooks').inspectHooks;

    // Jest can't run the workerized version of this module.
    const {
      flattenHooksList,
      loadSourceAndMetadata,
    } = require('../parseHookNames/loadSourceAndMetadata');
    const parseSourceAndMetadata =
      require('../parseHookNames/parseSourceAndMetadata').parseSourceAndMetadata;
    parseHookNames = async hooksTree => {
      const hooksList = flattenHooksList(hooksTree);

      // Runs in the UI thread so it can share Network cache:
      const locationKeyToHookSourceAndMetadata =
        await loadSourceAndMetadata(hooksList);

      // Runs in a Worker because it's CPU intensive:
      return parseSourceAndMetadata(
        hooksList,
        locationKeyToHookSourceAndMetadata,
      );
    };

    // Jest (jest-runner?) configures Errors to automatically account for source maps.
    // This changes behavior between our tests and the browser.
    // Ideally we would clear the prepareStackTrace() method on the Error object,
    // but Node falls back to looking for it on the main context's Error constructor,
    // which may still be patched.
    // To ensure we get the default behavior, override prepareStackTrace ourselves.
    // NOTE: prepareStackTrace is called from the error.stack getter, but the getter
    // has a recursion breaker which falls back to the default behavior.
    Error.prepareStackTrace = (error, trace) => {
      return error.stack;
    };
  });

  afterEach(() => {
    fetch.resetMocks();
  });

  async function getHookNamesForComponent(Component, props = {}) {
    const hooksTree = inspectHooks(Component, props, undefined, true);
    const hookNames = await parseHookNames(hooksTree);
    return hookNames;
  }

  it('should parse names for useState()', async () => {
    const Component =
      require('./__source__/__untransformed__/ComponentWithUseState').Component;
    const hookNames = await getHookNamesForComponent(Component);
    expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz', null]);
  });

  it('should parse names for useReducer()', async () => {
    const Component =
      require('./__source__/__untransformed__/ComponentWithUseReducer').Component;
    const hookNames = await getHookNamesForComponent(Component);
    expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz']);
  });

  it('should skip loading source files for unnamed hooks like useEffect', async () => {
    const Component =
      require('./__source__/__untransformed__/ComponentWithUseEffect').Component;

    // Since this component contains only unnamed hooks, the source code should not even be loaded.
    fetchMock.mockIf(/.+$/, request => {
      throw Error(`Unexpected file request for "${request.url}"`);
    });

    const hookNames = await getHookNamesForComponent(Component);
    expectHookNamesToEqual(hookNames, []); // No hooks with names
  });

  it('should skip loading source files for unnamed hooks like useEffect (alternate)', async () => {
    const Component =
      require('./__source__/__untransformed__/ComponentWithExternalUseEffect').Component;

    fetchMock.mockIf(/.+$/, request => {
      // Since the custom hook contains only unnamed hooks, the source code should not be loaded.
      if (request.url.endsWith('useCustom.js')) {
        throw Error(`Unexpected file request for "${request.url}"`);
      }
      return requireText(request.url, 'utf8');
    });

    const hookNames = await getHookNamesForComponent(Component);
    expectHookNamesToEqual(hookNames, ['count', null]); // No hooks with names
  });

  it('should parse names for custom hooks', async () => {
    const Component =
      require('./__source__/__untransformed__/ComponentWithNamedCustomHooks').Component;
    const hookNames = await getHookNamesForComponent(Component);
    expectHookNamesToEqual(hookNames, [
      'foo',
      null, // Custom hooks can have names, but not when using destructuring.
      'baz',
    ]);
  });

  it('should parse names for code using hooks indirectly', async () => {
    const Component =
      require('./__source__/__untransformed__/ComponentUsingHooksIndirectly').Component;
    const hookNames = await getHookNamesForComponent(Component);
    expectHookNamesToEqual(hookNames, ['count', 'darkMode', 'isDarkMode']);
  });

  it('should parse names for code using nested hooks', async () => {
    const Component =
      require('./__source__/__untransformed__/ComponentWithNestedHooks').Component;
    let InnerComponent;
    const hookNames = await getHookNamesForComponent(Component, {
      callback: innerComponent => {
        InnerComponent = innerComponent;
      },
    });
    const innerHookNames = await getHookNamesForComponent(InnerComponent);
    expectHookNamesToEqual(hookNames, ['InnerComponent']);
    expectHookNamesToEqual(innerHookNames, ['state']);
  });

  it('should return null for custom hooks without explicit names', async () => {
    const Component =
      require('./__source__/__untransformed__/ComponentWithUnnamedCustomHooks').Component;
    const hookNames = await getHookNamesForComponent(Component);
    expectHookNamesToEqual(hookNames, [
      null, // Custom hooks can have names, but this one does not even return a value.
      null, // Custom hooks can have names, but not when using destructuring.
      null, // Custom hooks can have names, but not when using destructuring.
    ]);
  });

  // TODO Test that cache purge works

  // TODO Test that cached metadata is purged when Fast Refresh scheduled

  describe('inline, external and bundle source maps', () => {
    it('should work for simple components', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'count', // useState
        ]);
      }

      await test('./__source__/Example'); // original source (uncompiled)
      await test('./__source__/__compiled__/inline/Example'); // inline source map
      await test('./__source__/__compiled__/external/Example'); // external source map
      await test('./__source__/__compiled__/inline/index-map/Example'); // inline index map source map
      await test('./__source__/__compiled__/external/index-map/Example'); // external index map source map
      await test('./__source__/__compiled__/bundle/index', 'Example'); // bundle source map
      await test('./__source__/__compiled__/no-columns/Example'); // simulated Webpack 'cheap-module-source-map'
    });

    it('should work with more complex files and components', async () => {
      async function test(path, name = undefined) {
        const components = name != null ? require(path)[name] : require(path);

        let hookNames = await getHookNamesForComponent(components.List);
        expectHookNamesToEqual(hookNames, [
          'newItemText', // useState
          'items', // useState
          'uid', // useState
          'handleClick', // useCallback
          'handleKeyPress', // useCallback
          'handleChange', // useCallback
          'removeItem', // useCallback
          'toggleItem', // useCallback
        ]);

        hookNames = await getHookNamesForComponent(components.ListItem, {
          item: {},
        });
        expectHookNamesToEqual(hookNames, [
          'handleDelete', // useCallback
          'handleToggle', // useCallback
        ]);
      }

      await test('./__source__/ToDoList'); // original source (uncompiled)
      await test('./__source__/__compiled__/inline/ToDoList'); // inline source map
      await test('./__source__/__compiled__/external/ToDoList'); // external source map
      await test('./__source__/__compiled__/inline/index-map/ToDoList'); // inline index map source map
      await test('./__source__/__compiled__/external/index-map/ToDoList'); // external index map source map
      await test('./__source__/__compiled__/bundle', 'ToDoList'); // bundle source map
      await test('./__source__/__compiled__/no-columns/ToDoList'); // simulated Webpack 'cheap-module-source-map'
    });

    it('should work for custom hook', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'count', // useState()
          'isDarkMode', // useIsDarkMode()
          'isDarkMode', // useIsDarkMode -> useState()
          null, // useFoo()
        ]);
      }

      await test('./__source__/ComponentWithCustomHook'); // original source (uncompiled)
      await test('./__source__/__compiled__/inline/ComponentWithCustomHook'); // inline source map
      await test('./__source__/__compiled__/external/ComponentWithCustomHook'); // external source map
      await test(
        './__source__/__compiled__/inline/index-map/ComponentWithCustomHook',
      ); // inline index map source map
      await test(
        './__source__/__compiled__/external/index-map/ComponentWithCustomHook',
      ); // external index map source map
      await test('./__source__/__compiled__/bundle', 'ComponentWithCustomHook'); // bundle source map
      await test(
        './__source__/__compiled__/no-columns/ComponentWithCustomHook',
      ); // simulated Webpack 'cheap-module-source-map'
    });

    it('should work when code is using hooks indirectly', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'count', // useState()
          'darkMode', // useDarkMode()
          'isDarkMode', // useState()
        ]);
      }

      await test(
        './__source__/__compiled__/inline/ComponentUsingHooksIndirectly',
      ); // inline source map
      await test(
        './__source__/__compiled__/external/ComponentUsingHooksIndirectly',
      ); // external source map
      await test(
        './__source__/__compiled__/inline/index-map/ComponentUsingHooksIndirectly',
      ); // inline index map source map
      await test(
        './__source__/__compiled__/external/index-map/ComponentUsingHooksIndirectly',
      ); // external index map source map
      await test(
        './__source__/__compiled__/bundle',
        'ComponentUsingHooksIndirectly',
      ); // bundle source map
      await test(
        './__source__/__compiled__/no-columns/ComponentUsingHooksIndirectly',
      ); // simulated Webpack 'cheap-module-source-map'
    });

    it('should work when code is using nested hooks', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        let InnerComponent;
        const hookNames = await getHookNamesForComponent(Component, {
          callback: innerComponent => {
            InnerComponent = innerComponent;
          },
        });
        const innerHookNames = await getHookNamesForComponent(InnerComponent);
        expectHookNamesToEqual(hookNames, [
          'InnerComponent', // useMemo()
        ]);
        expectHookNamesToEqual(innerHookNames, [
          'state', // useState()
        ]);
      }

      await test('./__source__/__compiled__/inline/ComponentWithNestedHooks'); // inline source map
      await test('./__source__/__compiled__/external/ComponentWithNestedHooks'); // external source map
      await test(
        './__source__/__compiled__/inline/index-map/ComponentWithNestedHooks',
      ); // inline index map source map
      await test(
        './__source__/__compiled__/external/index-map/ComponentWithNestedHooks',
      ); // external index map source map
      await test(
        './__source__/__compiled__/bundle',
        'ComponentWithNestedHooks',
      ); // bundle source map
      await test(
        './__source__/__compiled__/no-columns/ComponentWithNestedHooks',
      ); // simulated Webpack 'cheap-module-source-map'
    });

    it('should work for external hooks', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'theme', // useTheme()
          'theme', // useContext()
        ]);
      }

      // We can't test the uncompiled source here, because it either needs to get transformed,
      // which would break the source mapping, or the import statements will fail.

      await test(
        './__source__/__compiled__/inline/ComponentWithExternalCustomHooks',
      ); // inline source map
      await test(
        './__source__/__compiled__/external/ComponentWithExternalCustomHooks',
      ); // external source map
      await test(
        './__source__/__compiled__/inline/index-map/ComponentWithExternalCustomHooks',
      ); // inline index map source map
      await test(
        './__source__/__compiled__/external/index-map/ComponentWithExternalCustomHooks',
      ); // external index map source map
      await test(
        './__source__/__compiled__/bundle',
        'ComponentWithExternalCustomHooks',
      ); // bundle source map
      await test(
        './__source__/__compiled__/no-columns/ComponentWithExternalCustomHooks',
      ); // simulated Webpack 'cheap-module-source-map'
    });

    it('should work when multiple hooks are on a line', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'a', // useContext()
          'b', // useContext()
          'c', // useContext()
          'd', // useContext()
        ]);
      }

      await test(
        './__source__/__compiled__/inline/ComponentWithMultipleHooksPerLine',
      ); // inline source map
      await test(
        './__source__/__compiled__/external/ComponentWithMultipleHooksPerLine',
      ); // external source map
      await test(
        './__source__/__compiled__/inline/index-map/ComponentWithMultipleHooksPerLine',
      ); // inline index map source map
      await test(
        './__source__/__compiled__/external/index-map/ComponentWithMultipleHooksPerLine',
      ); // external index map source map
      await test(
        './__source__/__compiled__/bundle',
        'ComponentWithMultipleHooksPerLine',
      ); // bundle source map

      async function noColumnTest(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'a', // useContext()
          'b', // useContext()
          null, // useContext()
          null, // useContext()
        ]);
      }

      // Note that this test is expected to only match the first two hooks
      // because the 3rd and 4th hook are on the same line,
      // and this type of source map doesn't have column numbers.
      await noColumnTest(
        './__source__/__compiled__/no-columns/ComponentWithMultipleHooksPerLine',
      ); // simulated Webpack 'cheap-module-source-map'
    });

    // TODO Inline require (e.g. require("react").useState()) isn't supported yet.
    // Maybe this isn't an important use case to support,
    // since inline requires are most likely to exist in compiled source (if at all).
    xit('should work for inline requires', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'count', // useState()
        ]);
      }

      await test('./__source__/InlineRequire'); // original source (uncompiled)
      await test('./__source__/__compiled__/inline/InlineRequire'); // inline source map
      await test('./__source__/__compiled__/external/InlineRequire'); // external source map
      await test('./__source__/__compiled__/inline/index-map/InlineRequire'); // inline index map source map
      await test('./__source__/__compiled__/external/index-map/InlineRequire'); // external index map source map
      await test('./__source__/__compiled__/bundle', 'InlineRequire'); // bundle source map
      await test('./__source__/__compiled__/no-columns/InlineRequire'); // simulated Webpack 'cheap-module-source-map'
    });

    it('should support sources that contain the string "sourceMappingURL="', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'count', // useState()
        ]);
      }

      // We expect the inline sourceMappingURL to be invalid in this case; mute the warning.
      console.warn = () => {};

      await test('./__source__/ContainingStringSourceMappingURL'); // original source (uncompiled)
      await test(
        './__source__/__compiled__/inline/ContainingStringSourceMappingURL',
      ); // inline source map
      await test(
        './__source__/__compiled__/external/ContainingStringSourceMappingURL',
      ); // external source map
      await test(
        './__source__/__compiled__/inline/index-map/ContainingStringSourceMappingURL',
      ); // inline index map source map
      await test(
        './__source__/__compiled__/external/index-map/ContainingStringSourceMappingURL',
      ); // external index map source map
      await test(
        './__source__/__compiled__/bundle',
        'ContainingStringSourceMappingURL',
      ); // bundle source map
      await test(
        './__source__/__compiled__/no-columns/ContainingStringSourceMappingURL',
      ); // simulated Webpack 'cheap-module-source-map'
    });
  });

  describe('extended source maps', () => {
    beforeEach(() => {
      const babelParser = require('@babel/parser');
      const generateHookMapModule = require('../generateHookMap');
      jest.spyOn(babelParser, 'parse');
      jest.spyOn(generateHookMapModule, 'decodeHookMap');
    });

    it('should work for simple components', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'count', // useState
        ]);
        expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0);
        expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled();
      }

      await test(
        './__source__/__compiled__/inline/fb-sources-extended/Example',
      ); // x_facebook_sources extended inline source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/Example',
      ); // x_facebook_sources extended external source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/Example',
      ); // x_react_sources extended inline source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/Example',
      ); // x_react_sources extended external source map

      // Using index map format for source maps
      await test(
        './__source__/__compiled__/inline/fb-sources-extended/index-map/Example',
      ); // x_facebook_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/index-map/Example',
      ); // x_facebook_sources extended external index map source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/index-map/Example',
      ); // x_react_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/index-map/Example',
      ); // x_react_sources extended external index map source map

      // TODO test no-columns and bundle cases with extended source maps
    });

    it('should work with more complex files and components', async () => {
      async function test(path, name = undefined) {
        const components = name != null ? require(path)[name] : require(path);

        let hookNames = await getHookNamesForComponent(components.List);
        expectHookNamesToEqual(hookNames, [
          'newItemText', // useState
          'items', // useState
          'uid', // useState
          'handleClick', // useCallback
          'handleKeyPress', // useCallback
          'handleChange', // useCallback
          'removeItem', // useCallback
          'toggleItem', // useCallback
        ]);

        hookNames = await getHookNamesForComponent(components.ListItem, {
          item: {},
        });
        expectHookNamesToEqual(hookNames, [
          'handleDelete', // useCallback
          'handleToggle', // useCallback
        ]);

        expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0);
        expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled();
      }

      await test(
        './__source__/__compiled__/inline/fb-sources-extended/ToDoList',
      ); // x_facebook_sources extended inline source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/ToDoList',
      ); // x_facebook_sources extended external source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/ToDoList',
      ); // x_react_sources extended inline source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/ToDoList',
      ); // x_react_sources extended external source map

      // Using index map format for source maps
      await test(
        './__source__/__compiled__/inline/fb-sources-extended/index-map/ToDoList',
      ); // x_facebook_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/index-map/ToDoList',
      ); // x_facebook_sources extended external index map source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/index-map/ToDoList',
      ); // x_react_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/index-map/ToDoList',
      ); // x_react_sources extended external index map source map

      // TODO test no-columns and bundle cases with extended source maps
    });

    it('should work for custom hook', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'count', // useState()
          'isDarkMode', // useIsDarkMode()
          'isDarkMode', // useIsDarkMode -> useState()
          null, // isFoo()
        ]);
        expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0);
        expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled();
      }

      await test(
        './__source__/__compiled__/inline/fb-sources-extended/ComponentWithCustomHook',
      ); // x_facebook_sources extended inline source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/ComponentWithCustomHook',
      ); // x_facebook_sources extended external source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/ComponentWithCustomHook',
      ); // x_react_sources extended inline source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/ComponentWithCustomHook',
      ); // x_react_sources extended external source map

      // Using index map format for source maps
      await test(
        './__source__/__compiled__/inline/fb-sources-extended/index-map/ComponentWithCustomHook',
      ); // x_facebook_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithCustomHook',
      ); // x_facebook_sources extended external index map source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/index-map/ComponentWithCustomHook',
      ); // x_react_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/index-map/ComponentWithCustomHook',
      ); // x_react_sources extended external index map source map

      // TODO test no-columns and bundle cases with extended source maps
    });

    it('should work when code is using hooks indirectly', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'count', // useState()
          'darkMode', // useDarkMode()
          'isDarkMode', // useState()
        ]);
        expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0);
        expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled();
      }

      await test(
        './__source__/__compiled__/inline/fb-sources-extended/ComponentUsingHooksIndirectly',
      ); // x_facebook_sources extended inline source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/ComponentUsingHooksIndirectly',
      ); // x_facebook_sources extended external source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/ComponentUsingHooksIndirectly',
      ); // x_react_sources extended inline source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/ComponentUsingHooksIndirectly',
      ); // x_react_sources extended external source map

      // Using index map format for source maps
      await test(
        './__source__/__compiled__/inline/fb-sources-extended/index-map/ComponentUsingHooksIndirectly',
      ); // x_facebook_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/index-map/ComponentUsingHooksIndirectly',
      ); // x_facebook_sources extended external index map source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/index-map/ComponentUsingHooksIndirectly',
      ); // x_react_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/index-map/ComponentUsingHooksIndirectly',
      ); // x_react_sources extended external index map source map

      // TODO test no-columns and bundle cases with extended source maps
    });

    it('should work when code is using nested hooks', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        let InnerComponent;
        const hookNames = await getHookNamesForComponent(Component, {
          callback: innerComponent => {
            InnerComponent = innerComponent;
          },
        });
        const innerHookNames = await getHookNamesForComponent(InnerComponent);
        expectHookNamesToEqual(hookNames, [
          'InnerComponent', // useMemo()
        ]);
        expectHookNamesToEqual(innerHookNames, [
          'state', // useState()
        ]);
        expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0);
        expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled();
      }

      await test(
        './__source__/__compiled__/inline/fb-sources-extended/ComponentWithNestedHooks',
      ); // x_facebook_sources extended inline source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/ComponentWithNestedHooks',
      ); // x_facebook_sources extended external source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/ComponentWithNestedHooks',
      ); // x_react_sources extended inline source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/ComponentWithNestedHooks',
      ); // x_react_sources extended external source map

      // Using index map format for source maps
      await test(
        './__source__/__compiled__/inline/fb-sources-extended/index-map/ComponentWithNestedHooks',
      ); // x_facebook_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithNestedHooks',
      ); // x_facebook_sources extended external index map source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/index-map/ComponentWithNestedHooks',
      ); // x_react_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/index-map/ComponentWithNestedHooks',
      ); // x_react_sources extended external index map source map

      // TODO test no-columns and bundle cases with extended source maps
    });

    it('should work for external hooks', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'theme', // useTheme()
          'theme', // useContext()
        ]);
        expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0);
        expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled();
      }

      // We can't test the uncompiled source here, because it either needs to get transformed,
      // which would break the source mapping, or the import statements will fail.

      await test(
        './__source__/__compiled__/inline/fb-sources-extended/ComponentWithExternalCustomHooks',
      ); // x_facebook_sources extended inline source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/ComponentWithExternalCustomHooks',
      ); // x_facebook_sources extended external source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/ComponentWithExternalCustomHooks',
      ); // x_react_sources extended inline source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/ComponentWithExternalCustomHooks',
      ); // x_react_sources extended external source map

      // Using index map format for source maps
      await test(
        './__source__/__compiled__/inline/fb-sources-extended/index-map/ComponentWithExternalCustomHooks',
      ); // x_facebook_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithExternalCustomHooks',
      ); // x_facebook_sources extended external index map source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/index-map/ComponentWithExternalCustomHooks',
      ); // x_react_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/index-map/ComponentWithExternalCustomHooks',
      ); // x_react_sources extended external index map source map

      // TODO test no-columns and bundle cases with extended source maps
    });

    it('should work when multiple hooks are on a line', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'a', // useContext()
          'b', // useContext()
          'c', // useContext()
          'd', // useContext()
        ]);
        expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0);
        expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled();
      }

      await test(
        './__source__/__compiled__/inline/fb-sources-extended/ComponentWithMultipleHooksPerLine',
      ); // x_facebook_sources extended inline source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/ComponentWithMultipleHooksPerLine',
      ); // x_facebook_sources extended external source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/ComponentWithMultipleHooksPerLine',
      ); // x_react_sources extended inline source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/ComponentWithMultipleHooksPerLine',
      ); // x_react_sources extended external source map

      // Using index map format for source maps
      await test(
        './__source__/__compiled__/inline/fb-sources-extended/index-map/ComponentWithMultipleHooksPerLine',
      ); // x_facebook_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithMultipleHooksPerLine',
      ); // x_facebook_sources extended external index map source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/index-map/ComponentWithMultipleHooksPerLine',
      ); // x_react_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/index-map/ComponentWithMultipleHooksPerLine',
      ); // x_react_sources extended external index map source map

      // TODO test no-columns and bundle cases with extended source maps
    });

    // TODO Inline require (e.g. require("react").useState()) isn't supported yet.
    // Maybe this isn't an important use case to support,
    // since inline requires are most likely to exist in compiled source (if at all).
    xit('should work for inline requires', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'count', // useState()
        ]);
        expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0);
        expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled();
      }

      await test(
        './__source__/__compiled__/inline/fb-sources-extended/InlineRequire',
      ); // x_facebook_sources extended inline source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/InlineRequire',
      ); // x_facebook_sources extended external source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/InlineRequire',
      ); // x_react_sources extended inline source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/InlineRequire',
      ); // x_react_sources extended external source map

      // Using index map format for source maps
      await test(
        './__source__/__compiled__/inline/fb-sources-extended/index-map/InlineRequire',
      ); // x_facebook_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/index-map/InlineRequire',
      ); // x_facebook_sources extended external index map source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/index-map/InlineRequire',
      ); // x_react_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/index-map/InlineRequire',
      ); // x_react_sources extended external index map source map

      // TODO test no-columns and bundle cases with extended source maps
    });

    it('should support sources that contain the string "sourceMappingURL="', async () => {
      async function test(path, name = 'Component') {
        const Component = require(path)[name];
        const hookNames = await getHookNamesForComponent(Component);
        expectHookNamesToEqual(hookNames, [
          'count', // useState()
        ]);
        expect(require('@babel/parser').parse).toHaveBeenCalledTimes(0);
        expect(require('../generateHookMap').decodeHookMap).toHaveBeenCalled();
      }

      // We expect the inline sourceMappingURL to be invalid in this case; mute the warning.
      console.warn = () => {};

      await test(
        './__source__/__compiled__/inline/fb-sources-extended/ContainingStringSourceMappingURL',
      ); // x_facebook_sources extended inline source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/ContainingStringSourceMappingURL',
      ); // x_facebook_sources extended external source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/ContainingStringSourceMappingURL',
      ); // x_react_sources extended inline source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/ContainingStringSourceMappingURL',
      ); // x_react_sources extended external source map

      // Using index map format for source maps
      await test(
        './__source__/__compiled__/inline/fb-sources-extended/index-map/ContainingStringSourceMappingURL',
      ); // x_facebook_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/fb-sources-extended/index-map/ContainingStringSourceMappingURL',
      ); // x_facebook_sources extended external index map source map
      await test(
        './__source__/__compiled__/inline/react-sources-extended/index-map/ContainingStringSourceMappingURL',
      ); // x_react_sources extended inline index map source map
      await test(
        './__source__/__compiled__/external/react-sources-extended/index-map/ContainingStringSourceMappingURL',
      ); // x_react_sources extended external index map source map

      // TODO test no-columns and bundle cases with extended source maps
    });
  });
});

describe('parseHookNames worker', () => {
  let inspectHooks;
  let parseHookNames;
  let workerizedParseSourceAndMetadataMock;

  beforeEach(() => {
    window.Worker = undefined;

    workerizedParseSourceAndMetadataMock = jest.fn();

    initFetchMock();

    jest.mock('../parseHookNames/parseSourceAndMetadata.worker.js', () => {
      return {
        __esModule: true,
        default: () => ({
          parseSourceAndMetadata: workerizedParseSourceAndMetadataMock,
        }),
      };
    });

    inspectHooks =
      require('react-debug-tools/src/ReactDebugHooks').inspectHooks;
    parseHookNames = require('../parseHookNames').parseHookNames;
  });

  async function getHookNamesForComponent(Component, props = {}) {
    const hooksTree = inspectHooks(Component, props, undefined, true);
    const hookNames = await parseHookNames(hooksTree);
    return hookNames;
  }

  it('should use worker', async () => {
    const Component =
      require('./__source__/__untransformed__/ComponentWithUseState').Component;

    window.Worker = true;

    // Reset module so mocked worker instance can be updated.
    jest.resetModules();
    parseHookNames = require('../parseHookNames').parseHookNames;

    await getHookNamesForComponent(Component);
    expect(workerizedParseSourceAndMetadataMock).toHaveBeenCalledTimes(1);
  });
});
