import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { readFile } from 'node:fs/promises';

describe('TypeScript Project', () => {
  it('should generate defined constants correctly', async () => {
    const project = new TestProject();
    project.addFile('entrypoints/unlisted.html', '<html></html>');

    await project.prepare();

    const output = await project.serializeFile('.wxt/types/globals.d.ts');
    expect(output).toMatchInlineSnapshot(`
      ".wxt/types/globals.d.ts
      ----------------------------------------
      // Generated by wxt
      interface ImportMetaEnv {
        readonly MANIFEST_VERSION: 2 | 3;
        readonly BROWSER: string;
        readonly CHROME: boolean;
        readonly FIREFOX: boolean;
        readonly SAFARI: boolean;
        readonly EDGE: boolean;
        readonly OPERA: boolean;
        readonly COMMAND: "build" | "serve";
        readonly ENTRYPOINT: string;
      }
      interface ImportMeta {
        readonly env: ImportMetaEnv
      }
      "
    `);
  });

  it('should augment the types for browser.runtime.getURL', async () => {
    const project = new TestProject();
    project.addFile('entrypoints/popup.html', '<html></html>');
    project.addFile('entrypoints/options.html', '<html></html>');
    project.addFile('entrypoints/sandbox.html', '<html></html>');

    await project.prepare();

    const output = await project.serializeFile('.wxt/types/paths.d.ts');
    expect(output).toMatchInlineSnapshot(`
      ".wxt/types/paths.d.ts
      ----------------------------------------
      // Generated by wxt
      import "wxt/browser";

      declare module "wxt/browser" {
        export type PublicPath =
          | ""
          | "/"
          | "/options.html"
          | "/popup.html"
          | "/sandbox.html"
        type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
        export interface WxtRuntime {
          getURL(path: PublicPath): string;
          getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
        }
      }
      "
    `);
  });

  it('should include CSS entrypoints in browser.runtime.getURL paths', async () => {
    const project = new TestProject();
    project.addFile('entrypoints/unlisted.html', '<html></html>');
    project.addFile(
      'entrypoints/plain.css',
      `body {
        color: red;
      }`,
    );
    project.addFile(
      'entrypoints/overlay.content.css',
      `body {
        color: blue;
      }`,
    );

    await project.prepare();

    const output = await project.serializeFile('.wxt/types/paths.d.ts');
    expect(output).toContain('| "/plain.css"');
    expect(output).toContain('| "/content-scripts/overlay.css"');
  });

  it('should support string and template-literal entries in prepare:publicPaths', async () => {
    const project = new TestProject();
    project.addFile('entrypoints/popup.html', '<html></html>');

    await project.prepare({
      hooks: {
        'prepare:publicPaths': (_, paths) => {
          paths.push('icons/16.png');
          paths.push({ type: 'string', path: '/icons/32.png' });
          paths.push({
            type: 'templateLiteral',
            path: '_favicon/?${string}',
          });
        },
      },
    });

    const output = await project.serializeFile('.wxt/types/paths.d.ts');
    expect(output).toMatchInlineSnapshot(`
      ".wxt/types/paths.d.ts
      ----------------------------------------
      // Generated by wxt
      import "wxt/browser";

      declare module "wxt/browser" {
        export type PublicPath =
          | ""
          | "/"
          | \`/_favicon/?\${string}\`
          | "/icons/32.png"
          | "/icons/16.png"
          | "/popup.html"
        type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
        export interface WxtRuntime {
          getURL(path: PublicPath): string;
          getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
        }
      }
      "
    `);
  });

  it('should augment the types for browser.i18n.getMessage', async () => {
    const project = new TestProject();
    project.addFile('entrypoints/unlisted.html', '<html></html>');
    project.addFile(
      'public/_locales/en/messages.json',
      JSON.stringify({
        prompt_for_name: {
          message: "What's your name?",
          description: "Ask for the user's name",
        },
        hello: {
          message: 'Hello, $USER$',
          description: 'Greet the user',
          placeholders: {
            user: {
              content: '$1',
              example: 'Cira',
            },
          },
        },
        bye: {
          message: 'Goodbye, $USER$. Come back to $OUR_SITE$ soon!',
          description: 'Say goodbye to the user',
          placeholders: {
            our_site: {
              content: 'Example.com',
            },
            user: {
              content: '$1',
              example: 'Cira',
            },
          },
        },
      }),
    );
    project.setConfigFileConfig({
      manifest: {
        default_locale: 'en',
      },
    });

    await project.prepare();

    const output = await project.serializeFile('.wxt/types/i18n.d.ts');
    expect(output).toMatchInlineSnapshot(`
      ".wxt/types/i18n.d.ts
      ----------------------------------------
      // Generated by wxt
      import "wxt/browser";

      declare module "wxt/browser" {
        /**
         * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
         */
        interface GetMessageOptions {
          /**
           * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
           */
          escapeLt?: boolean
        }

        export interface WxtI18n extends I18n.Static {
          /**
           * The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even non-localized extensions can use this message.
           * Note: You can't use this message in a manifest file.
           *
           * "<browser.runtime.id>"
           */
          getMessage(
            messageName: "@@extension_id",
            substitutions?: string | string[],
            options?: GetMessageOptions,
          ): string;
          /**
           * "<browser.i18n.getUiLocale()>"
           */
          getMessage(
            messageName: "@@ui_locale",
            substitutions?: string | string[],
            options?: GetMessageOptions,
          ): string;
          /**
           * The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.
           *
           * "<ltr|rtl>"
           */
          getMessage(
            messageName: "@@bidi_dir",
            substitutions?: string | string[],
            options?: GetMessageOptions,
          ): string;
          /**
           * If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "ltr".
           *
           * "<rtl|ltr>"
           */
          getMessage(
            messageName: "@@bidi_reversed_dir",
            substitutions?: string | string[],
            options?: GetMessageOptions,
          ): string;
          /**
           * If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "right".
           *
           * "<left|right>"
           */
          getMessage(
            messageName: "@@bidi_start_edge",
            substitutions?: string | string[],
            options?: GetMessageOptions,
          ): string;
          /**
           * If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "left".
           *
           * "<right|left>"
           */
          getMessage(
            messageName: "@@bidi_end_edge",
            substitutions?: string | string[],
            options?: GetMessageOptions,
          ): string;
          /**
           * Ask for the user's name
           *
           * "What's your name?"
           */
          getMessage(
            messageName: "prompt_for_name",
            substitutions?: string | string[],
            options?: GetMessageOptions,
          ): string;
          /**
           * Greet the user
           *
           * "Hello, $USER$"
           */
          getMessage(
            messageName: "hello",
            substitutions?: string | string[],
            options?: GetMessageOptions,
          ): string;
          /**
           * Say goodbye to the user
           *
           * "Goodbye, $USER$. Come back to $OUR_SITE$ soon!"
           */
          getMessage(
            messageName: "bye",
            substitutions?: string | string[],
            options?: GetMessageOptions,
          ): string;
          getMessage(
            messageName: "@@extension_id" | "@@ui_locale" | "@@bidi_dir" | "@@bidi_reversed_dir" | "@@bidi_start_edge" | "@@bidi_end_edge" | "prompt_for_name" | "hello" | "bye",
            substitutions?: string | string[],
            options?: GetMessageOptions,
          ): string;
        }
      }
      "
    `);
  });

  it('should reference all the required types in a single declaration file', async () => {
    const project = new TestProject();
    project.addFile('entrypoints/unlisted.html', '<html></html>');

    await project.prepare();

    const output = await project.serializeFile('.wxt/wxt.d.ts');
    expect(output).toMatchInlineSnapshot(`
      ".wxt/wxt.d.ts
      ----------------------------------------
      // Generated by wxt
      /// <reference types="wxt/vite-builder-env" />
      /// <reference path="./types/paths.d.ts" />
      /// <reference path="./types/i18n.d.ts" />
      /// <reference path="./types/globals.d.ts" />
      /// <reference path="./types/imports-module.d.ts" />
      /// <reference path="./types/imports.d.ts" />
      "
    `);
  });

  it('should generate a TSConfig file for the project', async () => {
    const project = new TestProject();
    project.addFile('entrypoints/unlisted.html', '<html></html>');

    await project.prepare();

    const output = await project.serializeFile('.wxt/tsconfig.json');
    expect(output).toMatchInlineSnapshot(`
      ".wxt/tsconfig.json
      ----------------------------------------
      {
        "compilerOptions": {
          "lib": [
            "ESNext",
            "DOM",
            "DOM.Iterable"
          ],
          "target": "ESNext",
          "module": "Preserve",
          "moduleDetection": "force",
          "moduleResolution": "Bundler",
          "allowImportingTsExtensions": true,
          "verbatimModuleSyntax": true,
          "noEmit": true,
          "strict": true,
          "skipLibCheck": true,
          "noFallthroughCasesInSwitch": true,
          "noUncheckedIndexedAccess": true,
          "noImplicitOverride": true,
          "paths": {
            "@": [
              ".."
            ],
            "@/*": [
              "../*"
            ],
            "~": [
              ".."
            ],
            "~/*": [
              "../*"
            ],
            "@@": [
              ".."
            ],
            "@@/*": [
              "../*"
            ],
            "~~": [
              ".."
            ],
            "~~/*": [
              "../*"
            ]
          }
        },
        "include": [
          "../**/*",
          "./wxt.d.ts"
        ],
        "exclude": [
          "../**/node_modules",
          "../.output"
        ]
      }"
    `);
  });

  it("should allow updating the project's tsconfig via hooks", async () => {
    const project = new TestProject();
    project.setConfigFileConfig({});
    project.addFile('entrypoints/unlisted.html', '<html></html>');

    await project.prepare({
      hooks: {
        'prepare:tsconfig': (_, { tsconfig }) => {
          tsconfig.test = 'test';
        },
      },
    });

    const actual = JSON.parse(
      await readFile(project.resolvePath('.wxt/tsconfig.json'), 'utf-8'),
    );
    expect(actual).toMatchObject({ test: 'test' });
  });

  it('should generate correct path aliases for a custom srcDir', async () => {
    const project = new TestProject();
    project.addFile('src/entrypoints/unlisted.html', '<html></html>');
    project.setConfigFileConfig({
      srcDir: 'src',
    });

    await project.prepare();

    const output = await project.serializeFile('.wxt/tsconfig.json');
    expect(output).toMatchInlineSnapshot(`
      ".wxt/tsconfig.json
      ----------------------------------------
      {
        "compilerOptions": {
          "lib": [
            "ESNext",
            "DOM",
            "DOM.Iterable"
          ],
          "target": "ESNext",
          "module": "Preserve",
          "moduleDetection": "force",
          "moduleResolution": "Bundler",
          "allowImportingTsExtensions": true,
          "verbatimModuleSyntax": true,
          "noEmit": true,
          "strict": true,
          "skipLibCheck": true,
          "noFallthroughCasesInSwitch": true,
          "noUncheckedIndexedAccess": true,
          "noImplicitOverride": true,
          "paths": {
            "@": [
              "../src"
            ],
            "@/*": [
              "../src/*"
            ],
            "~": [
              "../src"
            ],
            "~/*": [
              "../src/*"
            ],
            "@@": [
              ".."
            ],
            "@@/*": [
              "../*"
            ],
            "~~": [
              ".."
            ],
            "~~/*": [
              "../*"
            ]
          }
        },
        "include": [
          "../**/*",
          "./wxt.d.ts"
        ],
        "exclude": [
          "../**/node_modules",
          "../.output"
        ]
      }"
    `);
  });

  it('should add additional path aliases listed in the alias config, preventing defaults from being overridden', async () => {
    const project = new TestProject();
    project.addFile('src/entrypoints/unlisted.html', '<html></html>');
    project.setConfigFileConfig({
      srcDir: 'src',
      alias: {
        example: 'example',
        '@': 'ignored-path',
      },
    });

    await project.prepare();

    const output = await project.serializeFile('.wxt/tsconfig.json');
    expect(output).toMatchInlineSnapshot(`
      ".wxt/tsconfig.json
      ----------------------------------------
      {
        "compilerOptions": {
          "lib": [
            "ESNext",
            "DOM",
            "DOM.Iterable"
          ],
          "target": "ESNext",
          "module": "Preserve",
          "moduleDetection": "force",
          "moduleResolution": "Bundler",
          "allowImportingTsExtensions": true,
          "verbatimModuleSyntax": true,
          "noEmit": true,
          "strict": true,
          "skipLibCheck": true,
          "noFallthroughCasesInSwitch": true,
          "noUncheckedIndexedAccess": true,
          "noImplicitOverride": true,
          "paths": {
            "example": [
              "../example"
            ],
            "example/*": [
              "../example/*"
            ],
            "@": [
              "../src"
            ],
            "@/*": [
              "../src/*"
            ],
            "~": [
              "../src"
            ],
            "~/*": [
              "../src/*"
            ],
            "@@": [
              ".."
            ],
            "@@/*": [
              "../*"
            ],
            "~~": [
              ".."
            ],
            "~~/*": [
              "../*"
            ]
          }
        },
        "include": [
          "../**/*",
          "./wxt.d.ts"
        ],
        "exclude": [
          "../**/node_modules",
          "../.output"
        ]
      }"
    `);
  });

  it('should start path aliases with "./" for paths inside the .wxt dir', async () => {
    const project = new TestProject();
    project.addFile('src/entrypoints/unlisted.html', '<html></html>');
    project.setConfigFileConfig({
      srcDir: 'src',
      alias: {
        example: '.wxt/example.ts',
      },
    });

    await project.prepare();

    const output = await project.serializeFile('.wxt/tsconfig.json');
    expect(output).toContain('./example.ts');
  });

  it('should type `/_favicon/` paths when the `favicon` permission is declared', async () => {
    const project = new TestProject();
    project.addFile('entrypoints/popup.html', '<html></html>');

    await project.build({
      manifest: { permissions: ['favicon'] },
    });

    const pathsOutput = await project.serializeFile('.wxt/types/paths.d.ts');
    const manifest = await project.getOutputManifest();

    // The generated type must be a template-literal (backticks), not a
    // string-literal — otherwise query params (`?pageUrl=...&size=...`)
    // would not type-check.
    expect(pathsOutput).toMatchInlineSnapshot(`
      ".wxt/types/paths.d.ts
      ----------------------------------------
      // Generated by wxt
      import "wxt/browser";

      declare module "wxt/browser" {
        export type PublicPath =
          | ""
          | "/"
          | \`/_favicon/?\${string}\`
          | "/popup.html"
        type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
        export interface WxtRuntime {
          getURL(path: PublicPath): string;
          getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
        }
      }
      "
    `);

    // Per the review on #1570, WXT intentionally does NOT add a
    // `web_accessible_resources` entry for `_favicon/*`: that's only
    // needed for content-script usage, and users who need it can add
    // it themselves.
    expect(manifest.web_accessible_resources).toBeUndefined();
  });

  it('should set correct import.meta.env.BROWSER type based on targetBrowsers', async () => {
    const project = new TestProject();
    project.addFile('entrypoints/unlisted.html', '<html></html>');
    project.setConfigFileConfig({
      targetBrowsers: ['firefox', 'chrome'],
    });

    await project.prepare();

    const output = await project.serializeFile('.wxt/types/globals.d.ts');
    expect(output).toContain('readonly BROWSER: "firefox" | "chrome";');
  });

  // TODO: Once a module has been published, use it here for testing - local files are never added to the .wxt/wxt.d.ts file
  it.todo(
    'should add modules from NPM to the TS project if they have a configKey',
  );
});
