import { describe, expect, it, vi } from "vitest";
import { APPLICATION_VERSION } from "../../../../version";
import type { RegexExecutionRequest } from "../../../model/match";
import {
  JavaEngineAdapter,
  JAVA_BRIDGE_ABI_VERSION,
  JAVA_ENGINE_IDENTITY,
  JAVA_ENGINE_VERSION,
  JAVA_FLAVOUR_VERSION,
  javaApplicationFlags,
  normalizedJavaFlags,
} from "./JavaEngineAdapter";
import type { TeaVmJavaRegexModule } from "./java-module";

function fields(...values: readonly (string | number | boolean)[]): string {
  return values
    .map((value) => {
      const encoded =
        typeof value === "boolean" ? (value ? "1" : "0") : String(value);
      return `${encoded.length}:${encoded}`;
    })
    .join("");
}

function module(
  overrides: Partial<TeaVmJavaRegexModule> = {},
): TeaVmJavaRegexModule {
  return {
    bridgeAbiVersion: () => JAVA_BRIDGE_ABI_VERSION,
    engineIdentity: () => JAVA_ENGINE_IDENTITY,
    selfTest: () => 0,
    execute: () => fields("1", "ok", 0, false, 0),
    replace: () => fields("1", "ok", 0, false, 0, false, ""),
    ...overrides,
  };
}

function request(
  overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
  return {
    flavour: "java",
    flavourVersion: JAVA_FLAVOUR_VERSION,
    pattern: "(?<letter>a)(b)?",
    flags: ["g"],
    subject: "ab a",
    captureMetadata: [
      {
        number: 1,
        name: "letter",
        range: { startUtf16: 0, endUtf16: 12 },
        repeated: false,
      },
      {
        number: 2,
        range: { startUtf16: 12, endUtf16: 16 },
        repeated: false,
      },
    ],
    scanAll: false,
    maximumMatches: 100,
    maximumCaptureRows: 1_000,
    ...overrides,
  };
}

describe("TeaVM Java engine adapter", () => {
  it("loads the exact compatibility runtime identity", async () => {
    const adapter = new JavaEngineAdapter(module(), "unit-test TeaVM runtime");

    await expect(adapter.load()).resolves.toEqual(
      expect.objectContaining({
        flavour: "java",
        adapterVersion: APPLICATION_VERSION,
        engineName: "TeaVM java.util.regex",
        engineVersion: JAVA_ENGINE_VERSION,
        runtimeVersion: "unit-test TeaVM runtime",
        offsetUnit: "utf16",
      }),
    );
  });

  it("rejects a module that fails ABI identity or self-test", async () => {
    await expect(
      new JavaEngineAdapter(module({ bridgeAbiVersion: () => 99 })).load(),
    ).rejects.toThrow(/identity or bridge self-test/u);
    await expect(
      new JavaEngineAdapter(module({ selfTest: () => 1 })).load(),
    ).rejects.toThrow(/identity or bridge self-test/u);
  });

  it("normalizes adapter flags and maps only native TeaVM bits", () => {
    const normalized = normalizedJavaFlags(["U", "i"], true);
    expect(normalized).toEqual({
      userFlags: "iU",
      effectiveFlags: "giuU",
      internallyAddedIndicesFlag: false,
      internallyAddedGlobalFlag: true,
    });
    expect(javaApplicationFlags(normalized)).toBe(
      (1 << 0) | (1 << 3) | (1 << 5),
    );
    expect(javaApplicationFlags(normalizedJavaFlags(["d"], false))).toBe(
      1 << 6,
    );
    expect(() => normalizedJavaFlags(["i", "i"], false)).toThrow(
      /unsupported Java flags/u,
    );
    expect(() => normalizedJavaFlags(["y"], false)).toThrow(
      /unsupported Java flags/u,
    );
  });

  it("materializes UTF-16 offsets and numbered/named capture metadata", async () => {
    const execute = vi.fn(() =>
      fields("1", "ok", 2, false, 1, 0, 3, 0, 1, -1, -1),
    );
    const adapter = new JavaEngineAdapter(module({ execute }));
    const result = await adapter.execute(
      request({ pattern: "(?<letter>a)(😀)?", subject: "a😀" }),
    );

    expect(result.accepted).toBe(true);
    expect(result.matches[0]).toEqual(
      expect.objectContaining({
        value: "a😀",
        range: { startUtf16: 0, endUtf16: 3 },
        nativeRange: { start: 0, end: 3, unit: "utf16" },
      }),
    );
    expect(result.matches[0]?.captures).toEqual([
      expect.objectContaining({
        groupNumber: 1,
        groupName: "letter",
        value: "a",
        status: "participated",
        range: { startUtf16: 0, endUtf16: 1 },
      }),
      {
        groupNumber: 2,
        status: "did-not-participate",
      },
    ]);
    expect(execute).toHaveBeenCalledWith(
      "(?<letter>a)(😀)?",
      "a😀",
      0,
      true,
      100,
      1_000,
    );
  });

  it("reports authoritative compile failures at pattern UTF-16 offsets", async () => {
    const adapter = new JavaEngineAdapter(
      module({
        execute: () =>
          fields(
            "1",
            "compile-error",
            "PatternSyntaxException",
            "Unclosed group",
            8,
          ),
      }),
    );
    const result = await adapter.execute(
      request({ pattern: "abcdefgh(", subject: "" }),
    );

    expect(result.accepted).toBe(false);
    expect(result.diagnostics[0]).toEqual(
      expect.objectContaining({
        code: "compile-error",
        message: "PatternSyntaxException: Unclosed group",
        range: { startUtf16: 8, endUtf16: 9 },
        provenance: "reported",
      }),
    );
  });

  it("surfaces result caps and the U semantic boundary", async () => {
    const adapter = new JavaEngineAdapter(
      module({
        execute: () => fields("1", "ok", 0, true, 1, 0, 0),
      }),
    );
    const result = await adapter.execute(
      request({
        pattern: "(?=.)",
        flags: ["U"],
        subject: "x",
        captureMetadata: [],
      }),
    );

    expect(result.truncated).toBe(true);
    expect(result.diagnostics.map(({ code }) => code)).toEqual([
      "engine-compatibility",
      "result-limit",
    ]);
  });

  it("uses bounded UTF-8 replacement output and reports native errors", async () => {
    const adapter = new JavaEngineAdapter(
      module({
        replace: () => fields("1", "ok", 0, false, 0, false, "é😀x"),
      }),
    );
    const bounded = await adapter.replace({
      ...request({ pattern: "x", subject: "x", captureMetadata: [] }),
      replacement: "é😀x",
      maximumOutputBytes: 3,
    });
    expect(bounded.output).toBe("é");
    expect(bounded.outputBytes).toBe(2);
    expect(bounded.outputTruncated).toBe(true);
    expect(bounded.truncated).toBe(true);
    expect(bounded.execution.diagnostics.at(-1)?.code).toBe(
      "replacement-output-limit",
    );

    const failing = new JavaEngineAdapter(
      module({
        replace: () =>
          fields(
            "1",
            "replacement-error",
            "IllegalArgumentException",
            "TeaVM Matcher rejected the replacement.",
            -1,
          ),
      }),
    );
    const error = await failing.replace({
      ...request(),
      replacement: "${name}",
      maximumOutputBytes: 100,
    });
    expect(error.execution.accepted).toBe(false);
    expect(error.execution.diagnostics[0]?.code).toBe("replacement-error");
  });

  it("rejects malformed bridge data and requests outside shared bounds", async () => {
    const malformed = new JavaEngineAdapter(
      module({ execute: () => fields("1", "ok", 1, false, 1, 0, 1, -1, 0) }),
    );
    await expect(malformed.execute(request({ subject: "a" }))).rejects.toThrow(
      /inconsistent capture offsets/u,
    );

    const adapter = new JavaEngineAdapter(module());
    await expect(
      adapter.execute(request({ flavourVersion: "OpenJDK Pattern" })),
    ).rejects.toThrow(/version .* unsupported/u);
    await expect(
      adapter.execute(request({ flavour: "scala" })),
    ).rejects.toThrow("cannot execute scala");
    await expect(adapter.execute(request({ flags: [""] }))).rejects.toThrow(
      "unsupported Java flags",
    );
    await expect(
      adapter.execute(request({ options: { canonicalEquivalence: true } })),
    ).rejects.toThrow("does not accept engine options");
    await expect(
      adapter.execute(request({ maximumMatches: 10_001 })),
    ).rejects.toThrow(/bounded contract/u);
    await expect(
      adapter.replace({
        ...request(),
        replacement: "x",
        maximumOutputBytes: 0,
      }),
    ).rejects.toThrow(/bounded contract/u);
  });
});
