// @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 { beforeAll, describe, expect, it } from "vitest";
import {
  JavaEngineAdapter,
  JAVA_ENGINE_IDENTITY,
  JAVA_ENGINE_VERSION,
} from "../../src/regex/execution/adapters/java/JavaEngineAdapter";
import type { TeaVmJavaRegexModule } from "../../src/regex/execution/adapters/java/java-module";
import type {
  RegexExecutionRequest,
  RegexReplacementRequest,
} from "../../src/regex/model/match";

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

function request(
  pattern: string,
  subject: string,
  overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
  return {
    flavour: "java",
    flavourVersion: JAVA_ENGINE_IDENTITY,
    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_000,
    ...overrides,
  };
}

beforeAll(async () => {
  const imported = (await import(
    `${pathToFileURL(path.join(pack, "java-regex.mjs")).href}?conformance`
  )) as unknown as TeaVmJavaRegexModule;
  adapter = new JavaEngineAdapter(
    imported,
    "TeaVM JavaScript · Node.js conformance · not OpenJDK",
  );
  await adapter.load();
});

describe("pinned TeaVM 0.15.0 java.util.regex conformance", () => {
  it("loads the exact compatibility engine identity", async () => {
    await expect(adapter.load()).resolves.toEqual(
      expect.objectContaining({
        engineName: "TeaVM java.util.regex",
        engineVersion: JAVA_ENGINE_VERSION,
        runtimeVersion: expect.stringContaining("not OpenJDK"),
        offsetUnit: "utf16",
      }),
    );
  });

  it("returns native UTF-16 offsets and named/numbered metadata", async () => {
    const result = await adapter.execute(
      request("(?<emoji>😀)([A-Z]+)?", "😀AB 😀", {
        captureMetadata: [
          {
            number: 1,
            name: "emoji",
            range: { startUtf16: 0, endUtf16: 12 },
            repeated: false,
          },
          {
            number: 2,
            name: "tail",
            range: { startUtf16: 12, endUtf16: 21 },
            repeated: false,
          },
        ],
      }),
    );

    expect(result.accepted).toBe(true);
    expect(result.matches.map(({ range }) => range)).toEqual([
      { startUtf16: 0, endUtf16: 4 },
      { startUtf16: 5, endUtf16: 7 },
    ]);
    expect(result.matches[0]?.captures).toEqual([
      expect.objectContaining({
        groupNumber: 1,
        groupName: "emoji",
        value: "😀",
        range: { startUtf16: 0, endUtf16: 2 },
      }),
      expect.objectContaining({
        groupNumber: 2,
        groupName: "tail",
        value: "AB",
        range: { startUtf16: 2, endUtf16: 4 },
      }),
    ]);
    expect(result.matches[1]?.captures[1]).toEqual({
      groupNumber: 2,
      groupName: "tail",
      status: "did-not-participate",
    });
  });

  it("supports TeaVM lookbehind, numeric backreferences and atomic groups", async () => {
    const lookbehind = await adapter.execute(request("(?<=a)(b)\\1", "abb ab"));
    expect(lookbehind.matches.map(({ value }) => value)).toEqual(["bb"]);

    const atomic = await adapter.execute(request("(?>a|ab)c", "abc"));
    expect(atomic.matches).toHaveLength(0);
  });

  it("advances global zero-length matches in UTF-16 without looping", async () => {
    const result = await adapter.execute(request("(?=.)", "😀x"));
    expect(result.matches.map(({ range }) => range)).toEqual([
      { startUtf16: 0, endUtf16: 0 },
      { startUtf16: 1, endUtf16: 1 },
      { startUtf16: 2, endUtf16: 2 },
    ]);
  });

  it("maps i, m, s, u and x to TeaVM Pattern flags", async () => {
    const cases = [
      ["a", "A", ["i"]],
      ["ä", "Ä", ["i", "u"]],
      ["^b$", "a\nb\nc", ["m"]],
      ["a.b", "a\nb", ["s"]],
      ["a b # comment", "ab", ["x"]],
    ] 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 normalized = await adapter.execute(
      request("a", "A", { flags: ["i", "m", "s", "u", "x"] }),
    );
    expect(normalized.flags.effectiveFlags).toBe("imsux");
  });

  it("maps d to UNIX_LINES and reports the honest U compatibility boundary", async () => {
    const ordinaryLines = await adapter.execute(
      request(".", "\r", { flags: [] }),
    );
    const unixLines = await adapter.execute(
      request(".", "\r", { flags: ["d"] }),
    );
    expect(ordinaryLines.matches).toHaveLength(0);
    expect(unixLines.matches).toHaveLength(1);

    const result = await adapter.execute(
      request("\\w+", "é", { flags: ["d", "U"] }),
    );
    expect(result.accepted).toBe(true);
    expect(result.matches).toHaveLength(0);
    expect(result.flags.userFlags).toBe("dU");
    expect(result.flags.effectiveFlags).toBe("duU");
    expect(result.flags.internallyAddedIndicesFlag).toBe(false);
    expect(result.diagnostics[0]).toEqual(
      expect.objectContaining({
        code: "engine-compatibility",
        message: expect.stringContaining("does not implement OpenJDK"),
      }),
    );
  });

  it("uses TeaVM Matcher native single-digit $n replacement", async () => {
    const result = await adapter.replace(replacement("(a)", "a a", "<$1>"));
    expect(result.output).toBe("<a> <a>");
    expect(result.outputTruncated).toBe(false);
    expect(result.execution.matches).toHaveLength(2);

    const escaped = await adapter.replace(replacement("(a)", "a a", "\\$1"));
    expect(escaped.output).toBe("$1 $1");
  });

  it("rejects ${name}, which TeaVM 0.15.0 does not support", async () => {
    const unsupportedNamed = await adapter.replace(
      replacement("(?<name>a)", "a", "${name}"),
    );
    expect(unsupportedNamed.execution.accepted).toBe(false);
    expect(unsupportedNamed.execution.diagnostics[0]).toEqual(
      expect.objectContaining({
        code: "replacement-error",
        message: expect.stringMatching(/IllegalArgumentException/u),
      }),
    );
  });

  it("returns compile diagnostics and enforces match/capture/output caps", async () => {
    const invalid = await adapter.execute(request("(", "x"));
    expect(invalid.accepted).toBe(false);
    expect(invalid.diagnostics[0]).toEqual(
      expect.objectContaining({
        code: "compile-error",
        provenance: "reported",
      }),
    );

    const matchLimited = await adapter.execute(
      request("a", "aaa", { maximumMatches: 1 }),
    );
    expect(matchLimited.matches).toHaveLength(1);
    expect(matchLimited.truncated).toBe(true);

    const captureLimited = await adapter.execute(
      request("(a)(b)", "ab", {
        maximumCaptureRows: 1,
        captureMetadata: [
          {
            number: 1,
            range: { startUtf16: 0, endUtf16: 3 },
            repeated: false,
          },
          {
            number: 2,
            range: { startUtf16: 3, endUtf16: 6 },
            repeated: false,
          },
        ],
      }),
    );
    expect(captureLimited.matches).toHaveLength(0);
    expect(captureLimited.truncated).toBe(true);

    const outputLimited = await adapter.replace(
      replacement("a", "aaaa", "long", { maximumOutputBytes: 5 }),
    );
    expect(outputLimited.output).toBe("longl");
    expect(outputLimited.outputBytes).toBe(5);
    expect(outputLimited.outputTruncated).toBe(true);
  });

  it("ships internally consistent source identity, licence and checksums", async () => {
    const metadata = JSON.parse(
      await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
    ) as {
      engineName: string;
      engineVersion: string;
      semanticIdentity: string;
      source: {
        tag: string;
        commitSha1: string;
        license: string;
      };
    };
    expect(metadata).toEqual(
      expect.objectContaining({
        engineName: "TeaVM java.util.regex",
        engineVersion: "0.15.0",
        semanticIdentity: expect.stringContaining("not OpenJDK"),
        source: expect.objectContaining({
          tag: "0.15.0",
          commitSha1: "ee91b03e616c4b45401cd11fb0cd7eb0daf6649b",
          license: "Apache-2.0",
        }),
      }),
    );

    const checksumText = await readFile(path.join(pack, "SHA256SUMS"), "utf8");
    for (const line of checksumText.trim().split("\n")) {
      const [expected, name] = line.split(/ {2}/u);
      const contents = await readFile(path.join(pack, name ?? ""));
      expect(createHash("sha256").update(contents).digest("hex"), name).toBe(
        expected,
      );
    }
    expect(await readFile(path.join(pack, "LICENSE.txt"), "utf8")).toContain(
      "Apache License",
    );
  });
});
