import { describe, expect, it } from "vitest";
import {
  ECMASCRIPT_FLAVOUR_VERSION,
  executeEcmaScript,
  replaceEcmaScript,
} from "./EcmaScriptEngineAdapter";
import type { RegexExecutionRequest } from "../../../model/match";
import { DEFAULT_REGEX_LIMITS } from "../../request-limits";

function request(
  overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
  return {
    flavour: "ecmascript",
    flavourVersion: ECMASCRIPT_FLAVOUR_VERSION,
    pattern: "(?<word>\\p{Letter}+)",
    flags: ["g", "u"],
    subject: "Grüße 2026",
    captureMetadata: [
      {
        number: 1,
        name: "word",
        range: { startUtf16: 0, endUtf16: 21 },
        repeated: false,
      },
    ],
    scanAll: false,
    maximumMatches: 10_000,
    maximumCaptureRows: 100_000,
    ...overrides,
  };
}

describe("native ECMAScript adapter", () => {
  it("uses actual indices and identifies its runtime", () => {
    const result = executeEcmaScript(request());

    expect(result.accepted).toBe(true);
    expect(result.engine.engineName).toBe("Native ECMAScript RegExp");
    expect(result.engine.offsetUnit).toBe("utf16");
    expect(result.engine.capabilities.benchmark).toBe(true);
    expect(result.matches).toHaveLength(1);
    expect(result.matches[0]?.range).toEqual({
      startUtf16: 0,
      endUtf16: 5,
    });
    expect(result.matches[0]?.captures[0]).toEqual(
      expect.objectContaining({
        groupNumber: 1,
        groupName: "word",
        value: "Grüße",
        status: "participated",
        range: { startUtf16: 0, endUtf16: 5 },
      }),
    );
    expect(result.flags.userFlags).toBe("gu");
    expect(result.flags.effectiveFlags).toContain("d");
    expect(result.flags.internallyAddedIndicesFlag).toBe(true);
  });

  it("distinguishes unmatched and matched-empty captures", () => {
    const result = executeEcmaScript(
      request({
        pattern: "(?:(a)|())",
        flags: [],
        subject: "",
        captureMetadata: [
          {
            number: 1,
            range: { startUtf16: 0, endUtf16: 3 },
            repeated: false,
          },
          {
            number: 2,
            range: { startUtf16: 4, endUtf16: 6 },
            repeated: false,
          },
        ],
      }),
    );

    expect(result.matches[0]?.captures[0]?.status).toBe("did-not-participate");
    expect(result.matches[0]?.captures[1]).toEqual(
      expect.objectContaining({
        status: "matched-empty",
        value: "",
        range: { startUtf16: 0, endUtf16: 0 },
      }),
    );
  });

  it("preserves only the final retained repeated capture", () => {
    const result = executeEcmaScript(
      request({
        pattern: "(a)+",
        flags: [],
        subject: "aaa",
        captureMetadata: [
          {
            number: 1,
            range: { startUtf16: 0, endUtf16: 3 },
            repeated: true,
          },
        ],
      }),
    );
    expect(result.matches[0]?.captures[0]).toEqual(
      expect.objectContaining({
        value: "a",
        range: { startUtf16: 2, endUtf16: 3 },
      }),
    );
  });

  it("advances zero-length Unicode matches by code point", () => {
    const result = executeEcmaScript(
      request({
        pattern: "(?=.)",
        flags: ["g", "u"],
        subject: "😀x",
        captureMetadata: [],
      }),
    );
    expect(result.matches.map((match) => match.range.startUtf16)).toEqual([
      0, 2,
    ]);
  });

  it("honours sticky behavior and explicit scan-all behavior", () => {
    const sticky = executeEcmaScript(
      request({
        pattern: "a",
        flags: ["y"],
        subject: "ba",
        captureMetadata: [],
      }),
    );
    expect(sticky.matches).toHaveLength(0);

    const stickySingle = executeEcmaScript(
      request({
        pattern: "a",
        flags: ["y"],
        subject: "aa",
        captureMetadata: [],
      }),
    );
    expect(stickySingle.matches).toHaveLength(1);

    const stickyScan = executeEcmaScript(
      request({
        pattern: "a",
        flags: ["y"],
        subject: "aa",
        captureMetadata: [],
        scanAll: true,
      }),
    );
    expect(stickyScan.matches).toHaveLength(2);

    const scan = executeEcmaScript(
      request({
        pattern: "a",
        flags: [],
        subject: "a a",
        captureMetadata: [],
        scanAll: true,
      }),
    );
    expect(scan.matches).toHaveLength(2);
    expect(scan.flags.userFlags).toBe("");
    expect(scan.flags.internallyAddedGlobalFlag).toBe(true);
  });

  it("uses native replacement semantics and applies an output limit", () => {
    const result = replaceEcmaScript({
      ...request({
        pattern: "(?<word>\\w+)",
        flags: ["g"],
        subject: "alice bob",
      }),
      replacement: "[$<word>]",
      maximumOutputBytes: 13,
    });
    expect(result.output).toBe("[alice] [bob]");
    expect(result.outputTruncated).toBe(false);
    expect(result.truncated).toBe(false);

    const limited = replaceEcmaScript({
      ...request({ pattern: "a", flags: ["g"], subject: "aaaa" }),
      replacement: "long",
      maximumOutputBytes: 5,
    });
    expect(limited.outputTruncated).toBe(true);
    expect(limited.truncated).toBe(true);
    expect(limited.outputBytes).toBeLessThanOrEqual(5);
    expect(limited.execution.diagnostics.at(-1)?.code).toBe(
      "replacement-output-limit",
    );
  });

  it("replaces every authoritative sticky match during explicit scan-all", () => {
    const result = replaceEcmaScript({
      ...request({
        pattern: "a",
        flags: ["y"],
        subject: "aa",
        captureMetadata: [],
        scanAll: true,
      }),
      replacement: "x",
      maximumOutputBytes: 100,
    });

    expect(result.execution.matches).toHaveLength(2);
    expect(result.execution.matches.map((match) => match.range)).toEqual([
      { startUtf16: 0, endUtf16: 1 },
      { startUtf16: 1, endUtf16: 2 },
    ]);
    expect(result.execution.flags.userFlags).toBe("y");
    expect(result.execution.flags.internallyAddedGlobalFlag).toBe(false);
    expect(result.output).toBe("xx");
  });

  it.each([
    {
      name: "match count",
      overrides: { maximumMatches: 1 },
    },
    {
      name: "capture row count",
      overrides: {
        pattern: "(a)",
        captureMetadata: [
          {
            number: 1,
            range: { startUtf16: 0, endUtf16: 3 },
            repeated: false,
          },
        ],
        maximumCaptureRows: 1,
      },
    },
  ])(
    "replaces only returned matches when truncated by the $name limit",
    ({ overrides }) => {
      const result = replaceEcmaScript({
        ...request({
          pattern: "a",
          flags: ["g"],
          subject: "aa",
          captureMetadata: [],
          ...overrides,
        }),
        replacement: "x",
        maximumOutputBytes: 100,
      });

      expect(result.execution.matches).toHaveLength(1);
      expect(result.execution.truncated).toBe(true);
      expect(result.outputTruncated).toBe(false);
      expect(result.truncated).toBe(true);
      expect(result.output).toBe("xa");
    },
  );

  it("streams ECMAScript substitution tokens without constructing unbounded output", () => {
    const subject = "abc";
    const pattern = "(?<name>b)";
    const replacement = "$$|$&|$`|$'|$01|$1|$2|$9|$<name>|$<missing>";
    const result = replaceEcmaScript({
      ...request({
        pattern,
        flags: [],
        subject,
        captureMetadata: [
          {
            number: 1,
            name: "name",
            range: { startUtf16: 0, endUtf16: pattern.length },
            repeated: false,
          },
        ],
      }),
      replacement,
      maximumOutputBytes: 1_000,
    });
    expect(result.output).toBe(
      subject.replace(new RegExp(pattern), replacement),
    );

    const bounded = replaceEcmaScript({
      ...request({ pattern: "a", flags: ["g"], subject: "aaaa" }),
      replacement: "x".repeat(
        DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16,
      ),
      maximumOutputBytes: 5,
    });
    expect(bounded.output).toBe("xxxxx");
    expect(bounded.outputBytes).toBe(5);
    expect(bounded.truncated).toBe(true);
  });

  it("rejects a very large empty $& substitution before scanning it", () => {
    const replacement = "$&".repeat(
      DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16 / 2 + 1,
    );
    expect(() =>
      replaceEcmaScript({
        ...request({ pattern: "(?:)", flags: ["g"], subject: "" }),
        replacement,
        maximumOutputBytes: 5,
      }),
    ).toThrow(/Replacement template exceeds/u);
  });

  it("matches native replacement behavior across numbered and literal token edges", () => {
    const subject = "ab a";
    const pattern = "(a)(b)?";
    for (const replacement of [
      "$0",
      "$00",
      "$01",
      "$02",
      "$1",
      "$2",
      "$3",
      "$10",
      "$20",
      "$99",
      "$<name>",
      "$",
      "$$",
      "$&",
      "$`",
      "$'",
    ]) {
      const result = replaceEcmaScript({
        ...request({
          pattern,
          flags: ["g"],
          subject,
          captureMetadata: [
            {
              number: 1,
              range: { startUtf16: 0, endUtf16: 3 },
              repeated: false,
            },
            {
              number: 2,
              range: { startUtf16: 3, endUtf16: 7 },
              repeated: false,
            },
          ],
        }),
        replacement,
        maximumOutputBytes: 10_000,
      });
      expect(result.output, replacement).toBe(
        subject.replace(new RegExp(pattern, "g"), replacement),
      );
    }
  });

  it("returns authoritative native compile errors", () => {
    const result = executeEcmaScript(
      request({ pattern: "(", flags: [], captureMetadata: [] }),
    );
    expect(result.accepted).toBe(false);
    expect(result.diagnostics[0]?.source).toBe("execution-engine");
    expect(result.diagnostics[0]?.code).toBe("compile-error");
  });

  it("rejects mismatched profiles and malformed bounded requests", () => {
    expect(() =>
      executeEcmaScript(request({ flavourVersion: "ECMAScript 2024" })),
    ).toThrow(/version .* unsupported/u);
    expect(() => executeEcmaScript(request({ flavour: "python" }))).toThrow(
      "cannot execute python",
    );
    expect(() => executeEcmaScript(request({ flags: ["g", "g"] }))).toThrow(
      "unsupported ECMAScript flags",
    );
    expect(() => executeEcmaScript(request({ flags: ["u", "v"] }))).toThrow(
      "cannot be combined",
    );
    expect(() =>
      executeEcmaScript(request({ options: { unicode: true } })),
    ).toThrow("does not accept engine options");
    expect(() => executeEcmaScript(request({ maximumMatches: 0 }))).toThrow(
      "outside the bounded contract",
    );
    expect(() =>
      replaceEcmaScript({
        ...request(),
        replacement: "x",
        maximumOutputBytes: 0,
      }),
    ).toThrow("outside the bounded contract");

    const legacyRequest = {
      ...request({
        pattern: "x",
        flags: [],
        subject: "x",
        captureMetadata: [],
      }),
    };
    delete (legacyRequest as { flavourVersion?: string }).flavourVersion;
    expect(executeEcmaScript(legacyRequest).accepted).toBe(true);
  });

  it("truncates match results explicitly", () => {
    const result = executeEcmaScript(
      request({
        pattern: ".",
        flags: ["g"],
        subject: "abcd",
        captureMetadata: [],
        maximumMatches: 2,
      }),
    );
    expect(result.matches).toHaveLength(2);
    expect(result.truncated).toBe(true);
    expect(result.diagnostics.at(-1)?.code).toBe("result-limit");
  });

  it("labels clipped display values while retaining exact ranges", () => {
    const subject = "x".repeat(70_000);
    const result = executeEcmaScript(
      request({
        pattern: "(x+)",
        flags: [],
        subject,
        captureMetadata: [
          {
            number: 1,
            range: { startUtf16: 0, endUtf16: 4 },
            repeated: false,
          },
        ],
      }),
    );
    expect(result.matches[0]).toMatchObject({
      valueStatus: "truncated",
      range: { startUtf16: 0, endUtf16: subject.length },
    });
    expect(result.matches[0]?.captures[0]).toMatchObject({
      status: "truncated",
      range: { startUtf16: 0, endUtf16: subject.length },
    });
    expect(result.diagnostics.at(-1)?.code).toBe("result-value-preview-limit");
  });
});
