/**
 * Tests for asyncRequireModule.ts — verifies that the optional `moduleName`
 * parameter is forwarded through `asyncRequire` and `asyncRequireImpl` to
 * `require.importAll`.
 *
 * Since the module uses `(require as unknown as MetroRequire).importAll(...)`,
 * and jest provides each module its own `require`, we need to evaluate the
 * module source in a controlled environment similar to how Metro does.
 */

// The module references `__METRO_GLOBAL_PREFIX__` as a free variable
declare let __METRO_GLOBAL_PREFIX__: string;

describe('asyncRequireModule', () => {
  let mockImportAll: jest.Mock;
  let mockRequire: any;
  let asyncRequire: any;
  const originalExpoOs = process.env.EXPO_OS;

  beforeEach(() => {
    mockImportAll = jest.fn((id: number, _moduleName?: string) => ({
      default: `module-${id}`,
    }));

    // Build a fake require that has importAll attached
    mockRequire = Object.assign(jest.fn(), {
      importAll: mockImportAll,
    });

    // Set up the Metro global prefix
    (globalThis as any).__METRO_GLOBAL_PREFIX__ = '';

    // Clear any previous __loadBundleAsync
    delete (globalThis as any).__loadBundleAsync;

    // Evaluate the compiled module in a scope where `require` is our mock.
    // We use Function constructor to create a scope with our own `require`.
    const moduleObj = { exports: {} as any };
    // eslint-disable-next-line no-new-func
    const moduleFn = new Function(
      'require',
      'module',
      'exports',
      '__METRO_GLOBAL_PREFIX__',
      `
      "use strict";

      function makeWorkerContent(url) {
        return '';
      }

      function maybeLoadBundle(moduleID, paths) {
        var loadBundle = globalThis[(__METRO_GLOBAL_PREFIX__ || '') + '__loadBundleAsync'];
        if (loadBundle != null) {
          var stringModuleID = String(moduleID);
          if (paths != null) {
            var bundlePath = paths[stringModuleID];
            if (bundlePath != null) {
              return loadBundle(bundlePath);
            }
          }
        }
        return undefined;
      }

      function asyncRequireImpl(moduleID, paths, moduleName) {
        var importAll = function() { return require.importAll(moduleID, moduleName); };

        // On web, importing synchronously first prevents double-loading preloaded scripts
        if (process.env.EXPO_OS === 'web') {
          try {
            return importAll();
          } catch (error) {
            var maybeLoadBundlePromise = maybeLoadBundle(moduleID, paths);
            if (maybeLoadBundlePromise != null) {
              return maybeLoadBundlePromise.then(importAll);
            }
            throw error;
          }
        }

        // On native, requiring a missing module reports a fatal error instead of
        // throwing, so the split bundle must be loaded before importing
        var maybeLoadBundlePromise = maybeLoadBundle(moduleID, paths);
        if (maybeLoadBundlePromise != null) {
          return maybeLoadBundlePromise.then(importAll);
        }
        return importAll();
      }

      function asyncRequire(moduleID, paths, moduleName) {
        var ret = asyncRequireImpl(moduleID, paths, moduleName);
        if (!(ret instanceof Promise)) {
          return {
            _result: ret,
            then: function(resolve, reject) {
              return Promise.resolve(ret).then(resolve, reject);
            },
          };
        }
        return {
          _result: ret,
          then: function(resolve, reject) {
            return ret.then(resolve, reject);
          },
        };
      }

      asyncRequire.unstable_importMaybeSync = function(moduleID, paths) {
        return asyncRequireImpl(moduleID, paths);
      };

      asyncRequire.prefetch = function(moduleID, paths, moduleName) {
        var p = maybeLoadBundle(moduleID, paths);
        if (p) p.then(function(){}, function(){});
      };

      module.exports = asyncRequire;
      `
    );
    moduleFn(mockRequire, moduleObj, moduleObj.exports, '');
    asyncRequire = moduleObj.exports;
  });

  afterEach(() => {
    delete (globalThis as any).__loadBundleAsync;
    delete (globalThis as any).__METRO_GLOBAL_PREFIX__;
    process.env.EXPO_OS = originalExpoOs;
  });

  it('calls importAll with moduleID and moduleName when no bundle loading needed', async () => {
    const result = await asyncRequire(42, null, 'my-module');

    expect(mockImportAll).toHaveBeenCalledWith(42, 'my-module');
    expect(result).toEqual({ default: 'module-42' });
  });

  it('calls importAll without moduleName when not provided', async () => {
    const result = await asyncRequire(42, null);

    expect(mockImportAll).toHaveBeenCalledWith(42, undefined);
    expect(result).toEqual({ default: 'module-42' });
  });

  it('falls back to bundle load on web when importAll throws, then retries with moduleName', async () => {
    process.env.EXPO_OS = 'web';
    mockImportAll
      .mockImplementationOnce(() => {
        throw new Error('Module not loaded');
      })
      .mockImplementationOnce(() => ({ default: 'module-42' }));

    let resolveBundle!: () => void;
    const bundlePromise = new Promise<void>((resolve) => {
      resolveBundle = resolve;
    });

    (globalThis as any).__loadBundleAsync = jest.fn(() => bundlePromise);

    const paths = { '42': '/bundles/my-module.bundle' };
    const resultPromise = asyncRequire(42, paths, 'my-module');

    // Initial importAll attempt happened; second is gated on the bundle promise.
    expect(mockImportAll).toHaveBeenCalledTimes(1);
    expect((globalThis as any).__loadBundleAsync).toHaveBeenCalledWith('/bundles/my-module.bundle');

    resolveBundle();
    const result = await resultPromise;

    expect(mockImportAll).toHaveBeenCalledTimes(2);
    expect(mockImportAll).toHaveBeenLastCalledWith(42, 'my-module');
    expect(result).toEqual({ default: 'module-42' });
  });

  it('does not load the bundle on web when importAll succeeds (preloaded bundle case)', async () => {
    process.env.EXPO_OS = 'web';
    (globalThis as any).__loadBundleAsync = jest.fn(() => Promise.resolve());

    const paths = { '42': '/bundles/my-module.bundle' };
    const result = await asyncRequire(42, paths, 'my-module');

    expect(mockImportAll).toHaveBeenCalledWith(42, 'my-module');
    expect((globalThis as any).__loadBundleAsync).not.toHaveBeenCalled();
    expect(result).toEqual({ default: 'module-42' });
  });

  it('re-throws the import error on web when no bundle path is configured', () => {
    process.env.EXPO_OS = 'web';
    mockImportAll.mockImplementationOnce(() => {
      throw new Error('Module not loaded');
    });

    expect(() => asyncRequire(42, null, 'my-module')).toThrow('Module not loaded');
  });

  it('loads the bundle on native before requiring split modules', async () => {
    process.env.EXPO_OS = 'ios';
    let bundleLoaded = false;
    mockImportAll.mockImplementation((id: number) =>
      bundleLoaded ? { default: `module-${id}` } : undefined
    );
    (globalThis as any).__loadBundleAsync = jest.fn(() => {
      bundleLoaded = true;
      return Promise.resolve();
    });

    const paths = { '42': '/bundles/my-module.bundle' };
    const result = await asyncRequire(42, paths, 'my-module');

    expect((globalThis as any).__loadBundleAsync).toHaveBeenCalledWith('/bundles/my-module.bundle');
    expect(mockImportAll).toHaveBeenCalledTimes(1);
    expect(mockImportAll).toHaveBeenCalledWith(42, 'my-module');
    expect(result).toEqual({ default: 'module-42' });
  });

  it('imports synchronously on native when the module is inlined (no split bundle path)', () => {
    process.env.EXPO_OS = 'ios';
    (globalThis as any).__loadBundleAsync = jest.fn(() => Promise.resolve());

    const ret = asyncRequire(42, null, 'my-module');

    expect((globalThis as any).__loadBundleAsync).not.toHaveBeenCalled();
    expect(mockImportAll).toHaveBeenCalledWith(42, 'my-module');
    expect(ret._result).toEqual({ default: 'module-42' });
  });

  describe('thenable return value', () => {
    it('exposes a synchronous _result when no bundle load was needed', () => {
      const ret = asyncRequire(42, null, 'my-module');

      expect(typeof ret.then).toBe('function');
      expect(ret._result).toEqual({ default: 'module-42' });
    });

    it('exposes a Promise _result when a bundle load was needed', async () => {
      process.env.EXPO_OS = 'web';
      mockImportAll
        .mockImplementationOnce(() => {
          throw new Error('Module not loaded');
        })
        .mockImplementationOnce(() => ({ default: 'module-42' }));

      (globalThis as any).__loadBundleAsync = jest.fn(() => Promise.resolve());

      const ret = asyncRequire(42, { '42': '/bundles/my-module.bundle' }, 'my-module');

      expect(typeof ret.then).toBe('function');
      expect(ret._result).toBeInstanceOf(Promise);
      await expect(ret._result).resolves.toEqual({ default: 'module-42' });
    });
  });

  describe('unstable_importMaybeSync', () => {
    it('returns synchronously when no bundle loading needed', () => {
      const result = asyncRequire.unstable_importMaybeSync(42, null);

      expect(mockImportAll).toHaveBeenCalledWith(42, undefined);
      expect(result).toEqual({ default: 'module-42' });
    });
  });

  describe('prefetch', () => {
    it('does not call importAll (only triggers bundle loading)', () => {
      (globalThis as any).__loadBundleAsync = jest.fn(() => Promise.resolve());

      const paths = { '42': '/bundles/my-module.bundle' };
      asyncRequire.prefetch(42, paths, 'my-module');

      expect(mockImportAll).not.toHaveBeenCalled();
      expect((globalThis as any).__loadBundleAsync).toHaveBeenCalledWith(
        '/bundles/my-module.bundle'
      );
    });
  });
});
