'use strict';

const fs = require('fs');
const nodePath = require('path');
const inlinedHostConfigs = require('../shared/inlinedHostConfigs');

function resolveEntryFork(resolvedEntry, isFBBundle) {
  // Pick which entry point fork to use:
  // .modern.fb.js
  // .classic.fb.js
  // .fb.js
  // .stable.js
  // .experimental.js
  // .js
  // or any of those plus .development.js

  if (isFBBundle) {
    // FB builds for react-dom need to alias both react-dom and react-dom/client to the same
    // entrypoint since there is only a single build for them.
    if (
      resolvedEntry.endsWith('react-dom/index.js') ||
      resolvedEntry.endsWith('react-dom/client.js') ||
      resolvedEntry.endsWith('react-dom/unstable_testing.js')
    ) {
      let specifier;
      let entrypoint;
      if (resolvedEntry.endsWith('index.js')) {
        specifier = 'react-dom';
        entrypoint = __EXPERIMENTAL__
          ? 'src/ReactDOMFB.modern.js'
          : 'src/ReactDOMFB.js';
      } else if (resolvedEntry.endsWith('client.js')) {
        specifier = 'react-dom/client';
        entrypoint = __EXPERIMENTAL__
          ? 'src/ReactDOMFB.modern.js'
          : 'src/ReactDOMFB.js';
      } else {
        // must be unstable_testing
        specifier = 'react-dom/unstable_testing';
        entrypoint = __EXPERIMENTAL__
          ? 'src/ReactDOMTestingFB.modern.js'
          : 'src/ReactDOMTestingFB.js';
      }

      resolvedEntry = nodePath.join(resolvedEntry, '..', entrypoint);
      const developmentEntry = resolvedEntry.replace('.js', '.development.js');
      if (fs.existsSync(developmentEntry)) {
        return developmentEntry;
      }
      if (fs.existsSync(resolvedEntry)) {
        return resolvedEntry;
      }
      const fbReleaseChannel = __EXPERIMENTAL__ ? 'www-modern' : 'www-classic';
      throw new Error(
        `${fbReleaseChannel} tests are expected to alias ${specifier} to ${entrypoint} but this file was not found`
      );
    }
    const resolvedFBEntry = resolvedEntry.replace(
      '.js',
      __EXPERIMENTAL__ ? '.modern.fb.js' : '.classic.fb.js'
    );
    const devFBEntry = resolvedFBEntry.replace('.js', '.development.js');
    if (fs.existsSync(devFBEntry)) {
      return devFBEntry;
    }
    if (fs.existsSync(resolvedFBEntry)) {
      return resolvedFBEntry;
    }
    const resolvedGenericFBEntry = resolvedEntry.replace('.js', '.fb.js');
    if (fs.existsSync(resolvedGenericFBEntry)) {
      return resolvedGenericFBEntry;
    }
    // Even if it's a FB bundle we fallthrough to pick stable or experimental if we don't have an FB fork.
  }
  const resolvedForkedEntry = resolvedEntry.replace(
    '.js',
    __EXPERIMENTAL__ ? '.experimental.js' : '.stable.js'
  );
  const devForkedEntry = resolvedForkedEntry.replace('.js', '.development.js');
  if (fs.existsSync(devForkedEntry)) {
    return devForkedEntry;
  }
  if (fs.existsSync(resolvedForkedEntry)) {
    return resolvedForkedEntry;
  }
  const plainDevEntry = resolvedEntry.replace('.js', '.development.js');
  if (fs.existsSync(plainDevEntry)) {
    return plainDevEntry;
  }
  // Just use the plain .js one.
  return resolvedEntry;
}

function mockReact() {
  jest.mock('react', () => {
    const resolvedEntryPoint = resolveEntryFork(
      require.resolve('react'),
      global.__WWW__ || global.__XPLAT__,
      global.__DEV__
    );
    return jest.requireActual(resolvedEntryPoint);
  });
  // Make it possible to import this module inside
  // the React package itself.
  jest.mock('shared/ReactSharedInternals', () => {
    return jest.requireActual('react/src/ReactSharedInternalsClient');
  });
}

// When we want to unmock React we really need to mock it again.
global.__unmockReact = mockReact;

mockReact();

jest.mock('react/react.react-server', () => {
  // If we're requiring an RSC environment, use those internals instead.
  jest.mock('shared/ReactSharedInternals', () => {
    return jest.requireActual('react/src/ReactSharedInternalsServer');
  });
  const resolvedEntryPoint = resolveEntryFork(
    require.resolve('react/src/ReactServer'),
    global.__WWW__ || global.__XPLAT__,
    global.__DEV__
  );
  return jest.requireActual(resolvedEntryPoint);
});

// When testing the custom renderer code path through `react-reconciler`,
// turn the export into a function, and use the argument as host config.
const shimHostConfigPath = 'react-reconciler/src/ReactFiberConfig';
jest.mock('react-reconciler', () => {
  return config => {
    jest.mock(shimHostConfigPath, () => config);
    return jest.requireActual('react-reconciler');
  };
});
const shimServerStreamConfigPath = 'react-server/src/ReactServerStreamConfig';
const shimServerConfigPath = 'react-server/src/ReactFizzConfig';
const shimFlightServerConfigPath = 'react-server/src/ReactFlightServerConfig';
jest.mock('react-server', () => {
  return config => {
    jest.mock(shimServerStreamConfigPath, () => config);
    jest.mock(shimServerConfigPath, () => config);
    return jest.requireActual('react-server');
  };
});
jest.mock('react-server/flight', () => {
  return config => {
    jest.mock(shimServerStreamConfigPath, () => config);
    jest.mock(shimServerConfigPath, () => config);
    jest.mock('react-server/src/ReactFlightServerConfigBundlerCustom', () => ({
      isClientReference: config.isClientReference,
      isServerReference: config.isServerReference,
      getClientReferenceKey: config.getClientReferenceKey,
      resolveClientReferenceMetadata: config.resolveClientReferenceMetadata,
    }));
    jest.mock(shimFlightServerConfigPath, () =>
      jest.requireActual(
        'react-server/src/forks/ReactFlightServerConfig.custom'
      )
    );
    return jest.requireActual('react-server/flight');
  };
});
const shimFlightClientConfigPath = 'react-client/src/ReactFlightClientConfig';
jest.mock('react-client/flight', () => {
  return config => {
    jest.mock(shimFlightClientConfigPath, () => config);
    return jest.requireActual('react-client/flight');
  };
});

const configPaths = [
  'react-reconciler/src/ReactFiberConfig',
  'react-client/src/ReactFlightClientConfig',
  'react-server/src/ReactServerStreamConfig',
  'react-server/src/ReactFizzConfig',
  'react-server/src/ReactFlightServerConfig',
];

function mockAllConfigs(rendererInfo) {
  configPaths.forEach(path => {
    // We want the reconciler to pick up the host config for this renderer.
    jest.mock(path, () => {
      let idx = path.lastIndexOf('/');
      let forkPath = path.slice(0, idx) + '/forks' + path.slice(idx);
      let parts = rendererInfo.shortName.split('-');
      while (parts.length) {
        try {
          const candidate = `${forkPath}.${parts.join('-')}.js`;
          fs.statSync(nodePath.join(process.cwd(), 'packages', candidate));
          return jest.requireActual(candidate);
        } catch (error) {
          if (error.code !== 'ENOENT') {
            throw error;
          }
          // try without a part
        }
        parts.pop();
      }
      throw new Error(
        `Expected to find a fork for ${path} but did not find one.`
      );
    });
  });
}

// But for inlined host configs (such as React DOM, Native, etc), we
// mock their named entry points to establish a host config mapping.
inlinedHostConfigs.forEach(rendererInfo => {
  if (rendererInfo.shortName === 'custom') {
    // There is no inline entry point for the custom renderers.
    // Instead, it's handled by the generic `react-reconciler` entry point above.
    return;
  }
  rendererInfo.entryPoints.forEach(entryPoint => {
    jest.mock(entryPoint, () => {
      mockAllConfigs(rendererInfo);
      const resolvedEntryPoint = resolveEntryFork(
        require.resolve(entryPoint),
        global.__WWW__ || global.__XPLAT__,
        global.__DEV__
      );
      return jest.requireActual(resolvedEntryPoint);
    });
  });
});

jest.mock('react-server/src/ReactFlightServer', () => {
  // If we're requiring an RSC environment, use those internals instead.
  jest.mock('shared/ReactSharedInternals', () => {
    return jest.requireActual('react/src/ReactSharedInternalsServer');
  });
  return jest.requireActual('react-server/src/ReactFlightServer');
});

// Make it possible to import this module inside
// the ReactDOM package itself.
jest.mock('shared/ReactDOMSharedInternals', () =>
  jest.requireActual('react-dom/src/ReactDOMSharedInternals')
);

jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock'));
