// @vitest-environment node

import path from "node:path";
import { pathToFileURL } from "node:url";
import { beforeAll, describe, expect, it } from "vitest";
import {
  Pcre2EngineAdapter,
  PCRE2_ENGINE_VERSION,
} from "../../src/regex/execution/adapters/pcre2/Pcre2EngineAdapter";
import { Pcre2TraceAdapter } from "../../src/regex/execution/adapters/pcre2/Pcre2TraceAdapter";
import type { Pcre2ModuleFactory } from "../../src/regex/execution/adapters/pcre2/pcre2-module";
import type {
  RegexExecutionRequest,
  RegexReplacementRequest,
} from "../../src/regex/model/match";

const pack = path.resolve("public/engines/pcre2");
let adapter: Pcre2EngineAdapter;
let traceAdapter: Pcre2TraceAdapter;

function request(
  pattern: string,
  subject: string,
  overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
  return {
    flavour: "pcre2",
    flavourVersion: "PCRE2 10.47 8-bit WebAssembly",
    pattern,
    flags: ["g"],
    options: {
      matchLimit: 1_000_000,
      depthLimit: 1_000,
      heapLimitKib: 32_768,
    },
    subject,
    captureMetadata: [],
    scanAll: false,
    maximumMatches: 100,
    maximumCaptureRows: 1_000,
    ...overrides,
  };
}

beforeAll(async () => {
  const imported = (await import(
    `${pathToFileURL(path.join(pack, "pcre2.mjs")).href}?conformance`
  )) as { default: Pcre2ModuleFactory };
  const module = await imported.default({
    locateFile: (name) => path.join(pack, name),
    print: () => undefined,
    printErr: () => undefined,
  });
  adapter = new Pcre2EngineAdapter(module, "Node.js WebAssembly conformance");
  traceAdapter = new Pcre2TraceAdapter(
    module,
    "Node.js WebAssembly trace conformance",
  );
  await adapter.load();
  await traceAdapter.load();
});

describe("pinned PCRE2 10.47 WebAssembly conformance", () => {
  it("loads the exact production engine identity", async () => {
    await expect(adapter.load()).resolves.toEqual(
      expect.objectContaining({
        flavour: "pcre2",
        engineName: "PCRE2 WebAssembly",
        engineVersion: PCRE2_ENGINE_VERSION,
        offsetUnit: "utf8-byte",
      }),
    );
  });

  it.each([
    {
      name: "branch-reset groups",
      pattern: "(?|(a)|(b))-(\\d+)",
      subject: "b-42",
      match: "b-42",
      captures: ["b", "42"],
    },
    {
      name: "named recursion",
      pattern: "(?<parentheses>\\((?:[^()]|(?&parentheses))*\\))",
      subject: "x (a(b)c) y",
      match: "(a(b)c)",
      captures: ["(a(b)c)"],
    },
    {
      name: "match-start reset",
      pattern: "foo\\Kbar",
      subject: "foobar",
      match: "bar",
      captures: [],
    },
  ])("executes PCRE2-only $name", async (testCase) => {
    const result = await adapter.execute(
      request(testCase.pattern, testCase.subject),
    );
    expect(result.accepted).toBe(true);
    expect(result.matches[0]?.value).toBe(testCase.match);
    expect(
      result.matches[0]?.captures
        .filter((capture) => capture.status !== "did-not-participate")
        .map((capture) => capture.value),
    ).toEqual(testCase.captures);
  });

  it("preserves duplicate names from the native name table", async () => {
    const result = await adapter.execute(
      request("(?<value>a)|(?<value>b)", "b", {
        flags: ["J"],
      }),
    );
    expect(result.accepted).toBe(true);
    expect(result.matches[0]?.captures).toEqual([
      expect.objectContaining({
        groupNumber: 1,
        groupName: "value",
        status: "did-not-participate",
      }),
      expect.objectContaining({
        groupNumber: 2,
        groupName: "value",
        value: "b",
      }),
    ]);
  });

  it.each([
    ["i", "^abc$", "AbC", "AbC"],
    ["m", "^b$", "a\nb\nc", "b"],
    ["s", "a.b", "a\nb", "a\nb"],
    ["x", "a # ignored\n b", "ab", "ab"],
    ["U", "a.+b", "a1b2b", "a1b"],
  ] as const)(
    "applies the native %s flag",
    async (flag, pattern, subject, expected) => {
      const result = await adapter.execute(
        request(pattern, subject, { flags: [flag] }),
      );
      expect(result.accepted).toBe(true);
      expect(result.matches[0]?.value).toBe(expected);
    },
  );

  it("always uses PCRE2 UTF and Unicode-property semantics", async () => {
    const result = await adapter.execute(
      request("^\\w+$", "Grüße", { flags: [] }),
    );
    expect(result.accepted).toBe(true);
    expect(result.matches[0]?.value).toBe("Grüße");
  });

  it("normalizes astral UTF-8 offsets and terminates global empty matches", async () => {
    const result = await adapter.execute(request("(?=.)", "😀a"));
    expect(result.accepted).toBe(true);
    expect(result.matches.map((match) => match.range)).toEqual([
      { startUtf16: 0, endUtf16: 0 },
      { startUtf16: 2, endUtf16: 2 },
    ]);
    expect(result.matches.map((match) => match.nativeRange)).toEqual([
      { start: 0, end: 0, unit: "utf8-byte" },
      { start: 4, end: 4, unit: "utf8-byte" },
    ]);
  });

  it("uses PCRE2's own CRLF-aware next-match progression", async () => {
    const result = await adapter.execute(request("(*CRLF)(?m)^", "\r\nx"));
    expect(result.accepted).toBe(true);
    expect(result.matches.map((match) => match.nativeRange.start)).toEqual([
      0, 2,
    ]);
  });

  it("executes native named substitution and reports exact output bytes", async () => {
    const replacement: RegexReplacementRequest = {
      ...request("(?<word>\\p{L}+)", "Grüße 42"),
      replacement: "<${word}>",
      maximumOutputBytes: 1_024,
    };
    const result = await adapter.replace(replacement);
    expect(result.execution.accepted).toBe(true);
    expect(result.output).toBe("<Grüße> 42");
    expect(result.outputBytes).toBe(
      new TextEncoder().encode(result.output).length,
    );
    expect(result.truncated).toBe(false);
  });

  it("executes PCRE2 subject, highest-capture and mark substitutions", async () => {
    const result = await adapter.replace({
      ...request("(*MARK:route)(a)(b)", "ab", { flags: [] }),
      replacement: "$_:$+:$*MARK",
      maximumOutputBytes: 1_024,
    });
    expect(result.execution.accepted).toBe(true);
    expect(result.output).toBe("ab:b:route");
  });

  it("marks match and replacement bounds without returning invented completeness", async () => {
    const limitedMatches = await adapter.execute(
      request("\\d", "1 2 3", { maximumMatches: 2 }),
    );
    expect(limitedMatches.matches.map((match) => match.value)).toEqual([
      "1",
      "2",
    ]);
    expect(limitedMatches.truncated).toBe(true);
    expect(limitedMatches.diagnostics).toEqual([
      expect.objectContaining({ code: "result-limit" }),
    ]);

    const limitedOutput = await adapter.replace({
      ...request("a", "aaaa"),
      replacement: "long",
      maximumOutputBytes: 5,
    });
    expect(limitedOutput.outputTruncated).toBe(true);
    expect(limitedOutput.truncated).toBe(true);
    expect(
      new TextEncoder().encode(limitedOutput.output).length,
    ).toBeLessThanOrEqual(5);
  });

  it("reports native compile and match-limit failures as failures", async () => {
    const compile = await adapter.execute(request("é(", ""));
    expect(compile.accepted).toBe(false);
    expect(compile.diagnostics[0]).toEqual(
      expect.objectContaining({
        code: "compile-error",
        range: { startUtf16: 2, endUtf16: 2 },
      }),
    );

    const limited = await adapter.execute(
      request("(a+)+$", `${"a".repeat(4_096)}!`, {
        flags: [],
        options: {
          matchLimit: 100,
          depthLimit: 100,
          heapLimitKib: 1_024,
        },
      }),
    );
    expect(limited.accepted).toBe(false);
    expect(limited.diagnostics[0]).toEqual(
      expect.objectContaining({ code: "match-limit" }),
    );
  });

  it("returns reported automatic callouts and only derived movement labels", async () => {
    const result = await traceAdapter.trace({
      flavour: "pcre2",
      pattern: "(*MARK:route)a+b",
      flags: [],
      options: {
        matchLimit: 1_000_000,
        depthLimit: 1_000,
        heapLimitKib: 32_768,
      },
      subject: "aaab",
      maximumTraceEvents: 1_000,
      maximumTraceBytes: 64 * 1024,
    });

    expect(result.accepted).toBe(true);
    expect(result.matched).toBe(true);
    expect(result.truncated).toBe(false);
    expect(result.events.length).toBeGreaterThan(2);
    expect(result.events.some((event) => event.reported.mark === "route")).toBe(
      true,
    );
    expect(result.events[0]).toEqual(
      expect.objectContaining({
        eventNumber: 1,
        reported: expect.objectContaining({
          provenance: "reported",
          calloutNumber: 255,
          patternPosition: expect.objectContaining({
            utf16: expect.any(Number),
            nativeByte: expect.any(Number),
          }),
        }),
        movement: expect.objectContaining({
          provenance: "derived",
          classification: "first-event",
        }),
      }),
    );
    expect(
      result.events.every(
        (event) =>
          event.reported.nextPatternItem.startUtf16 <=
          event.reported.nextPatternItem.endUtf16,
      ),
    ).toBe(true);
  });

  it("stops natively at the complete-event trace cap", async () => {
    const result = await traceAdapter.trace({
      flavour: "pcre2",
      pattern: "(a+)+$",
      flags: ["g"],
      options: {
        matchLimit: 1_000_000,
        depthLimit: 1_000,
        heapLimitKib: 32_768,
      },
      subject: "aaaa!",
      maximumTraceEvents: 1,
      maximumTraceBytes: 4_096,
    });

    expect(result.accepted).toBe(true);
    expect(result.events).toHaveLength(1);
    expect(result.totalEventCount).toBe(2);
    expect(result.lastCompleteEvent).toBe(1);
    expect(result.nativeMatchStatus).toBe(-37);
    expect(result.matched).toBe(false);
    expect(result.truncated).toBe(true);
    expect(result.diagnostics).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ code: "trace-single-invocation" }),
        expect.objectContaining({ code: "trace-limit" }),
      ]),
    );
  });

  it("normalizes trace compile errors to UTF-16", async () => {
    const result = await traceAdapter.trace({
      flavour: "pcre2",
      pattern: "é(",
      flags: [],
      subject: "",
      maximumTraceEvents: 100,
      maximumTraceBytes: 4_096,
    });
    expect(result.accepted).toBe(false);
    expect(result.diagnostics[0]).toEqual(
      expect.objectContaining({
        code: "compile-error",
        range: { startUtf16: 2, endUtf16: 2 },
      }),
    );
  });
});
