/// <reference types="node" />

import { expect, test, type Page } from "@playwright/test";
import { readdir } from "node:fs/promises";
import path from "node:path";

interface FlavourCase {
  readonly id:
    "php" | "perl" | "ruby" | "cpp" | "go" | "rust" | "dotnet" | "scala";
  readonly title: string;
  readonly pattern: string;
  readonly subject: string;
  readonly expectedMatch: string;
  readonly utf16Range: string;
  readonly nativeRange: string;
  readonly replacement: string;
  readonly expectedOutput: string;
  readonly capabilities: readonly string[];
  readonly timeoutMs: number;
  readonly expectedMatches?: number;
}

const FLAVOURS: readonly FlavourCase[] = [
  {
    id: "php",
    title: "PHP preg / PCRE2",
    pattern: "(?'word'[a-z]++)",
    subject: "😀 abc",
    expectedMatch: "abc",
    utf16Range: "3…6",
    nativeRange: "5…8 utf8-byte",
    replacement: "<$1>",
    expectedOutput: "😀 <abc>",
    capabilities: [
      "PHP preg (PCRE2)",
      "PHP 8.5.8 preg / PCRE2 10.44",
      "@php-wasm/web-8-5 3.1.46",
    ],
    timeoutMs: 60_000,
  },
  {
    id: "perl",
    title: "Perl 5.28.1 via legacy WebPerl",
    pattern: "(?<word>😀+)",
    subject: "x😀😀 y😀",
    expectedMatch: "😀😀",
    utf16Range: "1…5",
    nativeRange: "1…3 code-point",
    replacement: "${word}-$1-$$",
    expectedOutput: "x😀😀-😀😀-$ y😀-😀-$",
    capabilities: [
      "Perl regular expressions (legacy)",
      "5.28.1",
      "WebPerl 0.09-beta",
      "legacy beta",
      "classic worker",
    ],
    timeoutMs: 60_000,
    expectedMatches: 2,
  },
  {
    id: "ruby",
    title: "CRuby Regexp",
    pattern: "(?<word>[A-Z&&[^AEIOU]]+)",
    subject: "😀 BCD",
    expectedMatch: "BCD",
    utf16Range: "3…6",
    nativeRange: "2…5 code-point",
    replacement: "<\\k<word>>|\\&|\\`|\\'",
    expectedOutput: "😀 <BCD>|BCD|😀 |",
    capabilities: [
      "CRuby Regexp via ruby.wasm",
      "CRuby 4.0.0 Regexp",
      "ruby.wasm 2.9.3-2.9.4",
      "wasm32-wasi",
    ],
    timeoutMs: 60_000,
  },
  {
    id: "cpp",
    title: "C++ libc++ std::regex",
    pattern: "(\\w+)",
    subject: "😀 one",
    expectedMatch: "one",
    utf16Range: "3…6",
    nativeRange: "2…5 code-point",
    replacement: "<$1>",
    expectedOutput: "😀 <one>",
    capabilities: [
      "C++ std::wregex (Emscripten libc++)",
      "Emscripten 6.0.4 libc++ std::wregex",
      "32-bit wchar_t code-point bridge",
    ],
    timeoutMs: 30_000,
  },
  {
    id: "go",
    title: "Go standard-library regexp",
    pattern: "(?P<word>\\p{Greek}+)",
    subject: "😀 λλ",
    expectedMatch: "λλ",
    utf16Range: "3…5",
    nativeRange: "5…9 utf8-byte",
    replacement: "<${word}>",
    expectedOutput: "😀 <λλ>",
    capabilities: ["Go standard-library regexp", "1.26.5", "go1.26.5 js/wasm"],
    timeoutMs: 30_000,
  },
  {
    id: "rust",
    title: "Rust regex crate",
    pattern: "(?P<word>\\p{Greek}+)",
    subject: "😀 λλ",
    expectedMatch: "λλ",
    utf16Range: "3…5",
    nativeRange: "5…9 utf8-byte",
    replacement: "<${word}>",
    expectedOutput: "😀 <λλ>",
    capabilities: [
      "Rust regex crate",
      "1.13.1",
      "rustc 1.97.1 wasm32-unknown-unknown",
    ],
    timeoutMs: 30_000,
  },
  {
    id: "dotnet",
    title: ".NET System.Text.RegularExpressions",
    pattern: "(?<word>\\p{L}+)",
    subject: "😀 café",
    expectedMatch: "café",
    utf16Range: "3…7",
    nativeRange: "3…7 utf16",
    replacement: "<${word}>",
    expectedOutput: "😀 <café>",
    capabilities: [
      "System.Text.RegularExpressions",
      "10.0.10",
      ".NET browser WebAssembly",
      "CultureInvariant",
    ],
    timeoutMs: 60_000,
  },
  {
    id: "scala",
    title: "Scala/JVM compatibility profile",
    pattern: "(?<word>[a-z]++)",
    subject: "😀 abc",
    expectedMatch: "abc",
    utf16Range: "3…6",
    nativeRange: "3…6 utf16",
    replacement: "<$1>",
    expectedOutput: "😀 <abc>",
    capabilities: [
      "Scala/JVM regex via TeaVM java.util.regex",
      "TeaVM 0.15.0 compatibility profile",
      "Scala/JVM pattern and replacement semantics",
      "no Scala runtime",
    ],
    timeoutMs: 30_000,
  },
];

async function setEditor(page: Page, testId: string, value: string) {
  await page.getByTestId(testId).locator(".cm-content").fill(value);
}

async function waitForReady(page: Page, timeout: number) {
  await expect(page.locator(".run-status")).toHaveClass(/status-ready/u, {
    timeout,
  });
}

function observeRuntimeErrors(page: Page): string[] {
  const errors: string[] = [];
  page.on("pageerror", (error) => errors.push(`page: ${error.message}`));
  page.on("console", (message) => {
    const text = message.text();
    if (
      message.type() === "error" ||
      /content security policy|\bCSP\b|refused to (?:execute|load)/iu.test(text)
    ) {
      errors.push(`console ${message.type()}: ${text}`);
    }
  });
  return errors;
}

for (const flavour of FLAVOURS) {
  test(`${flavour.title} executes native syntax and replacement through its production worker`, async ({
    page,
  }) => {
    test.setTimeout(flavour.timeoutMs + 60_000);
    const runtimeErrors = observeRuntimeErrors(page);

    const response = await page.goto("/deep/nested/regex/");
    const csp = response?.headers()["content-security-policy"] ?? "";
    expect(csp).toContain("script-src 'self' 'wasm-unsafe-eval'");
    expect(csp).not.toContain("'unsafe-eval'");
    await waitForReady(page, 15_000);
    await page.getByLabel("Live").uncheck();
    await page
      .getByRole("combobox", { name: "Flavour" })
      .selectOption(flavour.id);
    await setEditor(page, "editor-regular-expression-pattern", flavour.pattern);
    await setEditor(page, "editor-test-text", flavour.subject);

    const run = page.getByRole("button", { name: "Run", exact: true });
    await expect(run).toBeEnabled({ timeout: 15_000 });
    await run.click();
    await waitForReady(page, flavour.timeoutMs);

    await expect(page.locator(".run-status")).toContainText(
      `native offsets ${flavour.nativeRange.split(" ").at(-1)}`,
    );
    const matches = page.locator('.extraction-row[aria-level="1"]');
    if (flavour.expectedMatches !== undefined) {
      await expect(matches).toHaveCount(flavour.expectedMatches);
    }
    const match = matches.first();
    await expect(match).toContainText(flavour.expectedMatch);
    await expect(match).toContainText(`UTF-16 range: ${flavour.utf16Range}`);
    await expect(match).toContainText(`native range: ${flavour.nativeRange}`);

    await page
      .getByRole("button", { name: "Capabilities", exact: true })
      .click();
    const capabilities = page.getByRole("dialog", { name: "Capabilities" });
    for (const identity of flavour.capabilities) {
      await expect(capabilities).toContainText(identity);
    }
    await capabilities
      .getByRole("button", { name: "Close capabilities" })
      .click();

    await page.getByRole("button", { name: "Replace", exact: true }).click();
    await setEditor(page, "editor-replacement-template", flavour.replacement);
    await expect(run).toBeEnabled({ timeout: 15_000 });
    await run.click();
    await waitForReady(page, flavour.timeoutMs);
    await expect(
      page.getByTestId("editor-replacement-output").locator(".cm-content"),
    ).toHaveText(flavour.expectedOutput);

    expect(runtimeErrors).toEqual([]);
  });
}

test("serves every additional engine module and WebAssembly dependency with production MIME types", async ({
  request,
}) => {
  const fixedAssets = [
    ["engines/php/php-loader.mjs", "text/javascript"],
    ["engines/php/php.wasm", "application/wasm"],
    ["engines/perl/perl-runtime.worker.js", "text/javascript"],
    ["engines/perl/emperl.js", "text/javascript"],
    ["engines/perl/emperl.wasm", "application/wasm"],
    ["engines/ruby/ruby.wasm", "application/wasm"],
    ["engines/cpp/cpp-regex.mjs", "text/javascript"],
    ["engines/cpp/cpp-regex.wasm", "application/wasm"],
    ["engines/go/wasm_exec.mjs", "text/javascript"],
    ["engines/go/go-regex.wasm", "application/wasm"],
    ["engines/rust/rust-regex.mjs", "text/javascript"],
    ["engines/rust/rust-regex_bg.wasm", "application/wasm"],
    ["engines/dotnet/_framework/dotnet.js", "text/javascript"],
    ["engines/java/java-regex.mjs", "text/javascript"],
  ] as const;

  for (const [asset, expectedMime] of fixedAssets) {
    const response = await request.get(`./${asset}`);
    expect(response.ok(), asset).toBe(true);
    expect(response.headers()["content-type"], asset).toContain(expectedMime);
    if (expectedMime === "application/wasm") {
      expect((await response.body()).subarray(0, 4), asset).toEqual(
        Buffer.from([0x00, 0x61, 0x73, 0x6d]),
      );
    }
  }

  const dotnetFramework = path.resolve(
    "public",
    "engines",
    "dotnet",
    "_framework",
  );
  const hashedDotnetDependencies = (await readdir(dotnetFramework))
    .filter((name) => /\.[a-z0-9]{10}\.(?:js|wasm)$/u.test(name))
    .sort();
  expect(hashedDotnetDependencies.length).toBeGreaterThan(2);
  expect(hashedDotnetDependencies.some((name) => name.endsWith(".js"))).toBe(
    true,
  );
  expect(hashedDotnetDependencies.some((name) => name.endsWith(".wasm"))).toBe(
    true,
  );

  for (const asset of hashedDotnetDependencies) {
    const response = await request.get(`./engines/dotnet/_framework/${asset}`);
    expect(response.ok(), asset).toBe(true);
    expect(response.headers()["content-type"], asset).toContain(
      asset.endsWith(".wasm") ? "application/wasm" : "text/javascript",
    );
  }
});
