// @vitest-environment node

import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
  RUST_ENGINE_VERSION,
  RUST_FLAVOUR_VERSION,
  RustEngineAdapter,
} from "../../src/regex/execution/adapters/rust/RustEngineAdapter";
import {
  RustWasmBridge,
  type RustRegexWasmModule,
} from "../../src/regex/execution/adapters/rust/RustWasmBridge";
import type {
  RegexExecutionRequest,
  RegexReplacementRequest,
} from "../../src/regex/model/match";

const pack = path.resolve("public/engines/rust");
let adapter: RustEngineAdapter;

function request(
  pattern: string,
  subject: string,
  overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
  return {
    flavour: "rust",
    flavourVersion: RUST_FLAVOUR_VERSION,
    pattern,
    flags: ["g"],
    subject,
    captureMetadata: [],
    scanAll: false,
    maximumMatches: 100,
    maximumCaptureRows: 1_000,
    ...overrides,
  };
}

function replacement(
  pattern: string,
  subject: string,
  replacementText: string,
  overrides: Partial<RegexReplacementRequest> = {},
): RegexReplacementRequest {
  return {
    ...request(pattern, subject),
    replacement: replacementText,
    maximumOutputBytes: 1_024,
    ...overrides,
  };
}

beforeAll(async () => {
  const imported = (await import(
    `${pathToFileURL(path.join(pack, "rust-regex.mjs")).href}?conformance`
  )) as RustRegexWasmModule;
  imported.initSync({
    module: await WebAssembly.compile(
      await readFile(path.join(pack, "rust-regex_bg.wasm")),
    ),
  });
  adapter = new RustEngineAdapter(
    new RustWasmBridge(imported),
    "Node.js conformance",
  );
  await adapter.load();
});

afterAll(() => {
  adapter?.terminate();
});

describe("Rust regex 1.13.1 WebAssembly conformance", () => {
  it("loads the exact crate and compiler identity", async () => {
    await expect(adapter.load()).resolves.toEqual(
      expect.objectContaining({
        flavour: "rust",
        engineName: "Rust regex crate",
        engineVersion: RUST_ENGINE_VERSION,
        runtimeVersion: expect.stringContaining(
          "rustc 1.97.1 wasm32-unknown-unknown",
        ),
        offsetUnit: "utf8-byte",
      }),
    );
  });

  it("returns native UTF-8 byte offsets and named captures", async () => {
    const result = await adapter.execute(
      request("(?P<emoji>😀+)([A-Z]+)?", "x😀😀AB 😀"),
    );
    expect(result.accepted).toBe(true);
    expect(
      result.matches.map(({ nativeRange, range }) => ({ nativeRange, range })),
    ).toEqual([
      {
        nativeRange: { start: 1, end: 11, unit: "utf8-byte" },
        range: { startUtf16: 1, endUtf16: 7 },
      },
      {
        nativeRange: { start: 12, end: 16, unit: "utf8-byte" },
        range: { startUtf16: 8, endUtf16: 10 },
      },
    ]);
    expect(result.matches[0]?.captures[0]).toEqual(
      expect.objectContaining({
        groupNumber: 1,
        groupName: "emoji",
        value: "😀😀",
        nativeRange: { start: 1, end: 9, unit: "utf8-byte" },
      }),
    );
    expect(result.matches[1]?.captures[1]).toEqual({
      groupNumber: 2,
      status: "did-not-participate",
    });
  });

  it("uses Rust's native Unicode default and rejects unsupported constructs", async () => {
    const unicode = await adapter.execute(
      request("^\\w+$", "Grüße", { flags: [] }),
    );
    expect(unicode.matches).toHaveLength(1);

    const unsupported = await adapter.execute(request("(?<=a)b", "ab"));
    expect(unsupported.accepted).toBe(false);
    expect(unsupported.diagnostics[0]).toEqual(
      expect.objectContaining({ code: "compile-error" }),
    );
  });

  it("maps i, m, s, U, u, x and R through RegexBuilder", async () => {
    const cases = [
      ["a", "A", ["i"]],
      ["^b$", "a\nb\nc", ["m"]],
      ["a.b", "a\nb", ["s"]],
      ["a+?", "aaa", ["U"]],
      ["^ a + $", "aaa", ["x"]],
      ["^b$", "a\r\nb\r\nc", ["m", "R"]],
      ["\\p{Greek}+", "λ", ["u"]],
    ] as const;
    for (const [pattern, subject, flags] of cases) {
      const result = await adapter.execute(
        request(pattern, subject, { flags: [...flags] }),
      );
      expect(result.matches, `${flags.join("")}: ${pattern}`).toHaveLength(1);
    }
    const ungreedy = await adapter.execute(
      request("a+", "aaa", { flags: ["U"] }),
    );
    expect(ungreedy.matches[0]?.value).toBe("a");
  });

  it("uses native Captures::expand replacement syntax", async () => {
    const result = await adapter.replace(
      replacement(
        "(?P<word>[a-z]+)-(\\d+)",
        "a-1 b-22",
        "${word}[$2]/$word/$$",
      ),
    );
    expect(result.execution.accepted).toBe(true);
    expect(result.output).toBe("a[1]/a/$ b[22]/b/$");

    const longest = await adapter.replace(replacement("(a)", "a", "$1x|${1}x"));
    expect(longest.output).toBe("|ax");
  });

  it("bounds capture amplification and preserves the untouched partial suffix", async () => {
    const bounded = await adapter.replace(
      replacement("(a+)", "a".repeat(256 * 1024), "$1".repeat(4_096), {
        maximumOutputBytes: 5,
      }),
    );
    expect(bounded.execution.accepted).toBe(true);
    expect(bounded.output).toBe("aaaaa");
    expect(bounded.outputBytes).toBe(5);
    expect(bounded.outputTruncated).toBe(true);

    const partial = await adapter.replace(
      replacement("a", "aaa", "x", { maximumMatches: 1 }),
    );
    expect(partial.execution.matches).toHaveLength(1);
    expect(partial.execution.truncated).toBe(true);
    expect(partial.output).toBe("xaa");
    expect(partial.outputTruncated).toBe(false);
  });

  it("enforces match/capture/output bounds without splitting UTF-8", async () => {
    const limited = await adapter.execute(
      request("a", "aaa", { maximumMatches: 1 }),
    );
    expect(limited.matches).toHaveLength(1);
    expect(limited.truncated).toBe(true);

    const rows = await adapter.execute(
      request("(a)(b)", "ab", { maximumCaptureRows: 1 }),
    );
    expect(rows.matches).toHaveLength(0);
    expect(rows.truncated).toBe(true);

    const output = await adapter.replace(
      replacement(".", "abc", "😀", { maximumOutputBytes: 5 }),
    );
    expect(output.output).toBe("😀");
    expect(output.outputBytes).toBe(4);
    expect(output.outputTruncated).toBe(true);
  });

  it("rejects lone UTF-16 surrogates before entering Rust", async () => {
    await expect(adapter.execute(request(".", "\ud800"))).rejects.toThrow(
      /unpaired UTF-16 surrogate/u,
    );
  });

  it("accepts control-heavy subjects within the decoded 16 MiB contract", async () => {
    const subject = "\u0000".repeat(3 * 1024 * 1024);
    const result = await adapter.execute(
      request("^\\x00+$", subject, {
        flags: [],
        maximumMatches: 1,
        maximumCaptureRows: 1,
      }),
    );
    expect(result.accepted).toBe(true);
    expect(result.matches[0]?.nativeRange).toEqual({
      start: 0,
      end: subject.length,
      unit: "utf8-byte",
    });
  });

  it("ships verified metadata, licences, Cargo.lock and checksums", async () => {
    const metadata = JSON.parse(
      await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
    ) as {
      engineVersion: string;
      source: { crateChecksumSha256: string; license: string };
      toolchain: {
        rustcCommitSha1: string;
        wasmBindgenVersion: string;
      };
    };
    expect(metadata).toEqual(
      expect.objectContaining({
        engineVersion: "1.13.1",
        source: expect.objectContaining({
          crateChecksumSha256:
            "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d",
          license: "MIT OR Apache-2.0",
        }),
        toolchain: expect.objectContaining({
          rustcCommitSha1: "8bab26f4f68e0e26f0bb7960be334d5b520ea452",
          wasmBindgenVersion: "0.2.126",
        }),
      }),
    );
    const checksums = await readFile(path.join(pack, "SHA256SUMS"), "utf8");
    for (const line of checksums.trim().split("\n")) {
      const [expected, name] = line.split(/ {2}/u);
      const value = await readFile(path.join(pack, name ?? ""));
      expect(createHash("sha256").update(value).digest("hex"), name).toBe(
        expected,
      );
    }
    expect(
      await readFile(path.join(pack, "LICENSE-MIT.txt"), "utf8"),
    ).toContain("Permission is hereby granted");
    expect(
      await readFile(path.join(pack, "LICENSE-Apache-2.0.txt"), "utf8"),
    ).toContain("Apache License");
  });
});
