import { BrowserWindow } from 'electron';

import { expect } from 'chai';

import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';

import { spawnAndWait } from './lib/spec-helpers';

const fixtureTimeout = 20000;
const fixtureKillTimeout = 5000;

const runFixture = async (appPath: string, args: string[] = []) => {
  return await spawnAndWait(process.execPath, [appPath, ...args], {
    timeout: fixtureTimeout,
    killTimeout: fixtureKillTimeout,
    stripOutput: true
  });
};

const fixturePath = path.resolve(__dirname, 'fixtures', 'esm');

describe('esm', () => {
  describe('main process', () => {
    it('should load an esm entrypoint', async () => {
      const result = await runFixture(path.resolve(fixturePath, 'entrypoint.mjs'));
      expect(result.code).to.equal(0);
      expect(result.stdout).to.equal('ESM Launch, ready: false');
    });

    it('should load an esm entrypoint based on type=module', async () => {
      const result = await runFixture(path.resolve(fixturePath, 'package'));
      expect(result.code).to.equal(0);
      expect(result.stdout).to.equal('ESM Package Launch, ready: false');
    });

    it('should wait for a top-level await before declaring the app ready', async () => {
      const result = await runFixture(path.resolve(fixturePath, 'top-level-await.mjs'));
      expect(result.code).to.equal(0);
      expect(result.stdout).to.equal('Top level await, ready: false');
    });

    it('should allow usage of pre-app-ready apis in top-level await', async () => {
      const result = await runFixture(path.resolve(fixturePath, 'pre-app-ready-apis.mjs'));
      expect(result.code).to.equal(0);
    });

    it('should allow use of dynamic import', async () => {
      const result = await runFixture(path.resolve(fixturePath, 'dynamic.mjs'));
      expect(result.code).to.equal(0);
      expect(result.stdout).to.equal('Exit with app, ready: false');
    });

    it("import 'electron/lol' should throw", async () => {
      const result = await runFixture(path.resolve(fixturePath, 'electron-modules', 'import-lol.mjs'));
      expect(result.code).to.equal(1);
      expect(result.stderr).to.match(/Error \[ERR_MODULE_NOT_FOUND\]/);
    });

    it("import 'electron/main' should not throw", async () => {
      const result = await runFixture(path.resolve(fixturePath, 'electron-modules', 'import-main.mjs'));
      expect(result.code).to.equal(0);
    });

    it("import 'electron/renderer' should not throw", async () => {
      const result = await runFixture(path.resolve(fixturePath, 'electron-modules', 'import-renderer.mjs'));
      expect(result.code).to.equal(0);
    });

    it("import 'electron/common' should not throw", async () => {
      const result = await runFixture(path.resolve(fixturePath, 'electron-modules', 'import-common.mjs'));
      expect(result.code).to.equal(0);
    });

    it("import 'electron/utility' should not throw", async () => {
      const result = await runFixture(path.resolve(fixturePath, 'electron-modules', 'import-utility.mjs'));
      expect(result.code).to.equal(0);
    });
  });

  describe('renderer process', () => {
    let w: BrowserWindow | null = null;
    const tempDirs: string[] = [];

    afterEach(async () => {
      if (w) w.close();
      w = null;
      while (tempDirs.length) {
        await fs.promises.rm(tempDirs.pop()!, { force: true, recursive: true });
      }
    });

    async function loadWindowWithPreload(preload: string, webPreferences: Electron.WebPreferences) {
      const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'e-spec-preload-'));
      tempDirs.push(tmpDir);
      const preloadPath = path.resolve(tmpDir, 'preload.mjs');
      await fs.promises.writeFile(preloadPath, preload);

      w = new BrowserWindow({
        show: false,
        webPreferences: {
          ...webPreferences,
          preload: preloadPath
        }
      });

      let error: Error | null = null;
      w.webContents.on('preload-error', (_, __, err) => {
        error = err;
      });

      await w.loadFile(path.resolve(fixturePath, 'empty.html'));

      return [w.webContents, error] as [Electron.WebContents, Error | null];
    }

    describe('nodeIntegration', () => {
      let badFilePath = '';

      beforeEach(async () => {
        badFilePath = path.resolve(path.resolve(os.tmpdir(), 'bad-file.badjs'));
        await fs.promises.writeFile(badFilePath, 'const foo = "bar";');
      });

      afterEach(async () => {
        await fs.promises.unlink(badFilePath);
      });

      it('should support an esm entrypoint', async () => {
        const [webContents] = await loadWindowWithPreload(
          'import { resolve } from "path"; window.resolvePath = resolve;',
          {
            nodeIntegration: true,
            sandbox: false,
            contextIsolation: false
          }
        );

        const exposedType = await webContents.executeJavaScript('typeof window.resolvePath');
        expect(exposedType).to.equal('function');
      });

      it('should delay load until the ESM import chain is complete', async () => {
        const [webContents] = await loadWindowWithPreload(
          `import { resolve } from "path";
        await new Promise(r => setTimeout(r, 500));
        window.resolvePath = resolve;`,
          {
            nodeIntegration: true,
            sandbox: false,
            contextIsolation: false
          }
        );

        const exposedType = await webContents.executeJavaScript('typeof window.resolvePath');
        expect(exposedType).to.equal('function');
      });

      it('should support a top-level await fetch blocking the page load', async () => {
        const [webContents] = await loadWindowWithPreload(
          `
        const r = await fetch("package/package.json");
        window.packageJson = await r.json();`,
          {
            nodeIntegration: true,
            sandbox: false,
            contextIsolation: false
          }
        );

        const packageJson = await webContents.executeJavaScript('window.packageJson');
        expect(packageJson).to.deep.equal(require('./fixtures/esm/package/package.json'));
      });

      const hostsUrl = pathToFileURL(
        process.platform === 'win32' ? 'C:\\Windows\\System32\\drivers\\etc\\hosts' : '/etc/hosts'
      );

      describe('without context isolation', () => {
        it('should use Blinks dynamic loader in the main world', async () => {
          const [webContents] = await loadWindowWithPreload('', {
            nodeIntegration: true,
            sandbox: false,
            contextIsolation: false
          });

          let error: Error | null = null;
          try {
            await webContents.executeJavaScript(`import(${JSON.stringify(hostsUrl)})`);
          } catch (err) {
            error = err as Error;
          }

          expect(error).to.not.equal(null);
          // This is a blink specific error message
          expect(error?.message).to.include('Failed to fetch dynamically imported module');
        });

        it('should use Node.js ESM dynamic loader in the preload', async () => {
          const [, preloadError] = await loadWindowWithPreload(
            `await import(${JSON.stringify(pathToFileURL(badFilePath))})`,
            {
              nodeIntegration: true,
              sandbox: false,
              contextIsolation: false
            }
          );

          expect(preloadError).to.not.equal(null);
          // This is a node.js specific error message
          expect(preloadError!.toString()).to.include('Unknown file extension');
        });

        it('should use import.meta callback handling from Node.js for Node.js modules', async () => {
          const result = await runFixture(path.resolve(fixturePath, 'import-meta'));
          expect(result.code).to.equal(0);
        });
      });

      describe('with context isolation', () => {
        it('should use Node.js ESM dynamic loader in the isolated context', async () => {
          const [, preloadError] = await loadWindowWithPreload(
            `await import(${JSON.stringify(pathToFileURL(badFilePath))})`,
            {
              nodeIntegration: true,
              sandbox: false,
              contextIsolation: true
            }
          );

          expect(preloadError).to.not.equal(null);
          // This is a node.js specific error message
          expect(preloadError!.toString()).to.include('Unknown file extension');
        });

        it('should use Blinks dynamic loader in the main world', async () => {
          const [webContents] = await loadWindowWithPreload('', {
            nodeIntegration: true,
            sandbox: false,
            contextIsolation: true
          });

          let error: Error | null = null;
          try {
            await webContents.executeJavaScript(`import(${JSON.stringify(hostsUrl)})`);
          } catch (err) {
            error = err as Error;
          }

          expect(error).to.not.equal(null);
          // This is a blink specific error message
          expect(error?.message).to.include('Failed to fetch dynamically imported module');
        });
      });
    });

    describe('without nodeIntegration and contextIsolation', () => {
      it('exposes Node globals to an ESM preload script', async () => {
        const [webContents] = await loadWindowWithPreload(
          `globalThis.preloadNodeGlobals = {
            processType: typeof process,
            bufferType: typeof Buffer,
            setImmediateType: typeof setImmediate,
            globalType: typeof global
          };`,
          {
            nodeIntegration: false,
            sandbox: false,
            contextIsolation: false
          }
        );

        const globals = await webContents.executeJavaScript('globalThis.preloadNodeGlobals');
        expect(globals).to.deep.equal({
          processType: 'object',
          bufferType: 'function',
          setImmediateType: 'function',
          globalType: 'object'
        });
      });

      it('removes Node globals from the renderer main world after the preload runs', async () => {
        const [webContents] = await loadWindowWithPreload('globalThis.preloadFinished = true;', {
          nodeIntegration: false,
          sandbox: false,
          contextIsolation: false
        });

        const pageGlobals = await webContents.executeJavaScript(`({
          preloadFinished: globalThis.preloadFinished,
          processType: typeof process,
          bufferType: typeof Buffer,
          setImmediateType: typeof setImmediate,
          globalType: typeof global
        })`);
        expect(pageGlobals).to.deep.equal({
          preloadFinished: true,
          processType: 'undefined',
          bufferType: 'undefined',
          setImmediateType: 'undefined',
          globalType: 'undefined'
        });
      });
    });

    describe('electron modules', () => {
      it("import 'electron/lol' should throw", async () => {
        const [, error] = await loadWindowWithPreload('import { ipcRenderer } from "electron/lol";', {
          sandbox: false
        });
        expect(error).to.not.equal(null);
        expect(error?.message).to.match(/Cannot find package 'electron'/);
      });

      it("import 'electron/main' should not throw", async () => {
        const [, error] = await loadWindowWithPreload('import { ipcRenderer } from "electron/main";', {
          sandbox: false
        });
        expect(error).to.equal(null);
      });

      it("import 'electron/renderer' should not throw", async () => {
        const [, error] = await loadWindowWithPreload('import { ipcRenderer } from "electron/renderer";', {
          sandbox: false
        });
        expect(error).to.equal(null);
      });

      it("import 'electron/common' should not throw", async () => {
        const [, error] = await loadWindowWithPreload('import { ipcRenderer } from "electron/common";', {
          sandbox: false
        });
        expect(error).to.equal(null);
      });

      it("import 'electron/utility' should not throw", async () => {
        const [, error] = await loadWindowWithPreload('import { ipcRenderer } from "electron/utility";', {
          sandbox: false
        });
        expect(error).to.equal(null);
      });
    });
  });
});
