import fs from 'node:fs';
import path from 'node:path';

import { afterEach, describe, expect, it } from 'vitest';

import { makeUniversalApp } from '../dist/index.js';
import { fsMove } from '../src/file-utils.js';
import {
  createStagingAppDir,
  generateNativeApp,
  templateApp,
  VERIFY_APP_TIMEOUT,
  verifyApp,
} from './util.js';
import { createPackage, createPackageWithOptions } from '@electron/asar';

const appsPath = path.resolve(import.meta.dirname, 'fixtures', 'apps');
const appsOutPath = path.resolve(import.meta.dirname, 'fixtures', 'apps', 'out');

// See `globalSetup.ts` for app fixture setup process
describe('makeUniversalApp', () => {
  afterEach(async () => {
    await fs.promises.rm(appsOutPath, { force: true, recursive: true });
    await fs.promises.mkdir(appsOutPath, { recursive: true });
  });

  it('throws an error if asar is only detected in one arch', async () => {
    const out = path.resolve(appsOutPath, 'Error.app');
    await expect(
      makeUniversalApp({
        x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
        arm64AppPath: path.resolve(appsPath, 'Arm64NoAsar.app'),
        outAppPath: out,
      }),
    ).rejects.toThrow(
      'Both the x64 and arm64 versions of your application need to have been built with the same asar settings (enabled vs disabled)',
    );
  });

  it('works for lipo binary resources', { timeout: VERIFY_APP_TIMEOUT }, async () => {
    const x64AppPath = await generateNativeApp({
      appNameWithExtension: 'LipoX64.app',
      arch: 'x64',
      createAsar: true,
    });
    const arm64AppPath = await generateNativeApp({
      appNameWithExtension: 'LipoArm64.app',
      arch: 'arm64',
      createAsar: true,
    });

    const out = path.resolve(appsOutPath, 'Lipo.app');
    await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out, mergeASARs: true });
    await verifyApp(out, true);
  });

  describe('force', () => {
    it('throws an error if `out` bundle already exists and `force` is `false`', async () => {
      const out = path.resolve(appsOutPath, 'Error.app');
      await fs.promises.mkdir(out, { recursive: true });
      await expect(
        makeUniversalApp({
          x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
          arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
          outAppPath: out,
        }),
      ).rejects.toThrow(/The out path ".*" already exists and force is not set to true/);
    });

    it(
      'packages successfully if `out` bundle already exists and `force` is `true`',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const out = path.resolve(appsOutPath, 'NoError.app');
        await fs.promises.mkdir(out, { recursive: true });
        await makeUniversalApp({
          x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
          arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
          outAppPath: out,
          force: true,
        });
        await verifyApp(out);
      },
    );
  });

  describe('asar mode', () => {
    it('should correctly merge two identical asars', { timeout: VERIFY_APP_TIMEOUT }, async () => {
      const out = path.resolve(appsOutPath, 'MergedAsar.app');
      await makeUniversalApp({
        x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
        arm64AppPath: path.resolve(appsPath, 'Arm64Asar.app'),
        outAppPath: out,
      });
      await verifyApp(out);
    });

    it(
      'should create a shim if asars are different between architectures',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const out = path.resolve(appsOutPath, 'ShimmedAsar.app');
        await makeUniversalApp({
          x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
          arm64AppPath: path.resolve(appsPath, 'Arm64AsarExtraFile.app'),
          outAppPath: out,
        });
        await verifyApp(out);
      },
    );

    it(
      'should merge two different asars when `mergeASARs` is enabled',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const out = path.resolve(appsOutPath, 'MergedAsar.app');
        await makeUniversalApp({
          x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
          arm64AppPath: path.resolve(appsPath, 'Arm64AsarExtraFile.app'),
          outAppPath: out,
          mergeASARs: true,
          singleArchFiles: 'extra-file.txt',
        });
        await verifyApp(out);
      },
    );

    it(
      'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique file',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const out = path.resolve(appsOutPath, 'Error.app');
        await expect(
          makeUniversalApp({
            x64AppPath: path.resolve(appsPath, 'X64Asar.app'),
            arm64AppPath: path.resolve(appsPath, 'Arm64AsarExtraFile.app'),
            outAppPath: out,
            mergeASARs: true,
            singleArchFiles: 'bad-rule',
          }),
        ).rejects.toThrow(/Detected unique file "extra-file\.txt"/);
      },
    );

    it(
      'should merge two different asars with native files when `mergeASARs` is enabled',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const x64AppPath = await generateNativeApp({
          appNameWithExtension: 'SingleArchFiles-x64.app',
          arch: 'x64',
          createAsar: true,
          singleArchBindings: true,
        });
        const arm64AppPath = await generateNativeApp({
          appNameWithExtension: 'SingleArchFiles-arm64.app',
          arch: 'arm64',
          createAsar: true,
          singleArchBindings: true,
        });
        const out = path.resolve(appsOutPath, 'SingleArchFiles.app');
        await makeUniversalApp({
          x64AppPath,
          arm64AppPath,
          outAppPath: out,
          mergeASARs: true,
          singleArchFiles: 'hello-world-*',
        });
        await verifyApp(out, true);
      },
    );

    it(
      'throws an error if `mergeASARs` is enabled and `singleArchFiles` is missing a unique native file',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const x64AppPath = await generateNativeApp({
          appNameWithExtension: 'SingleArchFiles-2-x64.app',
          arch: 'x64',
          createAsar: true,
          singleArchBindings: true,
        });
        const arm64AppPath = await generateNativeApp({
          appNameWithExtension: 'SingleArchFiles-2-arm64.app',
          arch: 'arm64',
          createAsar: true,
          singleArchBindings: true,
        });
        const out = path.resolve(appsOutPath, 'SingleArchFiles-2.app');
        await expect(
          makeUniversalApp({
            x64AppPath,
            arm64AppPath,
            outAppPath: out,
            mergeASARs: true,
            singleArchFiles: 'bad-rule',
          }),
        ).rejects.toThrow(
          /the number of mach-o files is not the same between the arm64 and x64 builds/,
        );
      },
    );

    it(
      'should not inject ElectronAsarIntegrity into `infoPlistsToIgnore`',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const arm64AppPath = await templateApp('Arm64-1.app', 'arm64', async (appPath) => {
          const { testPath } = await createStagingAppDir('Arm64-1');
          await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
          await templateApp('SubApp-1.app', 'arm64', async (subArm64AppPath) => {
            await fsMove(
              subArm64AppPath,
              path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)),
            );
          });
        });
        const x64AppPath = await templateApp('X64-1.app', 'x64', async (appPath) => {
          const { testPath } = await createStagingAppDir('X64-1');
          await createPackage(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app.asar'));
          await templateApp('SubApp-1.app', 'x64', async (subArm64AppPath) => {
            await fsMove(
              subArm64AppPath,
              path.resolve(appPath, 'Contents', 'Resources', path.basename(subArm64AppPath)),
            );
          });
        });
        const outAppPath = path.resolve(appsOutPath, 'UnmodifiedPlist.app');
        await makeUniversalApp({
          x64AppPath,
          arm64AppPath,
          outAppPath,
          mergeASARs: true,
          infoPlistsToIgnore: 'SubApp-1.app/Contents/Info.plist',
        });
        await verifyApp(outAppPath);
      },
    );

    // TODO: Investigate if this should even be allowed.
    // Current logic detects all unpacked files as APP_CODE, which doesn't seem correct since it could also be a macho file requiring lipo
    // https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49
    it.skip(
      'should shim asars with different unpacked dirs',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => {
          const { testPath } = await createStagingAppDir('UnpackedAppArm64');
          await createPackageWithOptions(
            testPath,
            path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
            {
              unpackDir: 'var',
              unpack: '*.txt',
            },
          );
        });

        const x64AppPath = await templateApp('UnpackedX64.app', 'x64', async (appPath) => {
          const { testPath } = await createStagingAppDir('UnpackedAppX64');
          await createPackageWithOptions(
            testPath,
            path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
            {},
          );
        });

        const outAppPath = path.resolve(appsOutPath, 'UnpackedDir.app');
        await makeUniversalApp({
          x64AppPath,
          arm64AppPath,
          outAppPath,
        });
        await verifyApp(outAppPath);
      },
    );

    it(
      'should generate AsarIntegrity for all asars in the application',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const { testPath } = await createStagingAppDir('app-2');
        const testAsarPath = path.resolve(appsOutPath, 'app-2.asar');
        await createPackage(testPath, testAsarPath);

        const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => {
          await fs.promises.copyFile(
            testAsarPath,
            path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
          );
          await fs.promises.copyFile(
            testAsarPath,
            path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'),
          );
        });
        const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => {
          await fs.promises.copyFile(
            testAsarPath,
            path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
          );
          await fs.promises.copyFile(
            testAsarPath,
            path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'),
          );
        });
        const outAppPath = path.resolve(appsOutPath, 'MultipleAsars.app');
        await makeUniversalApp({
          x64AppPath,
          arm64AppPath,
          outAppPath,
          mergeASARs: true,
        });
        await verifyApp(outAppPath);
      },
    );
  });

  describe('no asar mode', () => {
    it(
      'should correctly merge two identical app folders',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const out = path.resolve(appsOutPath, 'MergedNoAsar.app');
        await makeUniversalApp({
          x64AppPath: path.resolve(appsPath, 'X64NoAsar.app'),
          arm64AppPath: path.resolve(appsPath, 'Arm64NoAsar.app'),
          outAppPath: out,
        });
        await verifyApp(out);
      },
    );

    it('should shim two different app folders', { timeout: VERIFY_APP_TIMEOUT }, async () => {
      const arm64AppPath = await templateApp('ShimArm64.app', 'arm64', async (appPath) => {
        const { testPath } = await createStagingAppDir('shimArm64', {
          'i-aint-got-no-rhythm.bin': 'boomshakalaka',
        });
        await fs.promises.cp(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'), {
          recursive: true,
          verbatimSymlinks: true,
        });
      });

      const x64AppPath = await templateApp('ShimX64.app', 'x64', async (appPath) => {
        const { testPath } = await createStagingAppDir('shimX64', {
          'hello-world.bin': 'Hello World',
        });
        await fs.promises.cp(testPath, path.resolve(appPath, 'Contents', 'Resources', 'app'), {
          recursive: true,
          verbatimSymlinks: true,
        });
      });

      const outAppPath = path.resolve(appsOutPath, 'ShimNoAsar.app');
      await makeUniversalApp({
        x64AppPath,
        arm64AppPath,
        outAppPath,
      });
      await verifyApp(outAppPath);
    });

    it(
      'different app dirs with different macho files (shim and lipo)',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const x64AppPath = await generateNativeApp({
          appNameWithExtension: 'DifferentMachoAppX64-1.app',
          arch: 'x64',
          createAsar: false,
          additionalFiles: {
            'hello-world.bin': 'Hello World',
          },
        });
        const arm64AppPath = await generateNativeApp({
          appNameWithExtension: 'DifferentMachoAppArm64-1.app',
          arch: 'arm64',
          createAsar: false,
          additionalFiles: {
            'i-aint-got-no-rhythm.bin': 'boomshakalaka',
          },
        });

        const outAppPath = path.resolve(appsOutPath, 'DifferentMachoApp1.app');
        await makeUniversalApp({
          x64AppPath,
          arm64AppPath,
          outAppPath,
        });
        await verifyApp(outAppPath, true);
      },
    );

    it(
      "different app dirs with universal macho files (shim but don't lipo)",
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const x64AppPath = await generateNativeApp({
          appNameWithExtension: 'DifferentButUniversalMachoAppX64-2.app',
          arch: 'x64',
          createAsar: false,
          nativeModuleArch: 'universal',
          additionalFiles: {
            'hello-world.bin': 'Hello World',
          },
        });
        const arm64AppPath = await generateNativeApp({
          appNameWithExtension: 'DifferentButUniversalMachoAppArm64-2.app',
          arch: 'arm64',
          createAsar: false,
          nativeModuleArch: 'universal',
          additionalFiles: {
            'i-aint-got-no-rhythm.bin': 'boomshakalaka',
          },
        });

        const outAppPath = path.resolve(appsOutPath, 'DifferentButUniversalMachoApp.app');
        await makeUniversalApp({
          x64AppPath,
          arm64AppPath,
          outAppPath,
        });
        await verifyApp(outAppPath, true);
      },
    );

    it(
      'identical app dirs with different macho files (e.g. do not shim, but still lipo)',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const x64AppPath = await generateNativeApp({
          appNameWithExtension: 'DifferentMachoAppX64-2.app',
          arch: 'x64',
          createAsar: false,
        });
        const arm64AppPath = await generateNativeApp({
          appNameWithExtension: 'DifferentMachoAppArm64-2.app',
          arch: 'arm64',
          createAsar: false,
        });

        const out = path.resolve(appsOutPath, 'DifferentMachoApp2.app');
        await makeUniversalApp({
          x64AppPath,
          arm64AppPath,
          outAppPath: out,
        });
        await verifyApp(out, true);
      },
    );

    it(
      'identical app dirs with universal macho files (e.g., do not shim, just copy x64 dir)',
      { timeout: VERIFY_APP_TIMEOUT },
      async () => {
        const x64AppPath = await generateNativeApp({
          appNameWithExtension: 'UniversalMachoAppX64.app',
          arch: 'x64',
          createAsar: false,
          nativeModuleArch: 'universal',
        });
        const arm64AppPath = await generateNativeApp({
          appNameWithExtension: 'UniversalMachoAppArm64.app',
          arch: 'arm64',
          createAsar: false,
          nativeModuleArch: 'universal',
        });

        const out = path.resolve(appsOutPath, 'UniversalMachoApp.app');
        await makeUniversalApp({ x64AppPath, arm64AppPath, outAppPath: out });
        await verifyApp(out, true);
      },
    );
  });
});
