import { describe, expect, it } from "vitest";
import type {
  RegexExecutionRequest,
  RegexExecutionResult,
  RegexReplacementResult,
} from "../model/match";
import {
  MAXIMUM_REGEX_TEST_RESULT_MESSAGE_BYTES,
  type RegexTestExpectation,
} from "./test-case.types";
import { runRegexTest } from "./test-runner";

const rejectedExecution: RegexExecutionResult = {
  accepted: false,
  engine: {
    flavour: "ecmascript",
    adapterVersion: "test",
    engineName: "Test ECMAScript engine",
    engineVersion: "test",
    offsetUnit: "utf16",
    capabilities: {
      compilation: true,
      matching: true,
      replacement: true,
      namedCaptures: true,
      captureHistory: false,
      actualTrace: false,
      benchmark: false,
    },
  },
  flags: {
    userFlags: "",
    effectiveFlags: "",
    internallyAddedIndicesFlag: false,
    internallyAddedGlobalFlag: false,
  },
  matches: [],
  diagnostics: [
    {
      id: "compile-error",
      source: "execution-engine",
      severity: "error",
      code: "compile-error",
      message: "Unsupported regular-expression construct.",
      flavour: "ecmascript",
      provenance: "reported",
    },
  ],
  elapsedMs: 1,
  truncated: false,
};

const rejectedReplacement: RegexReplacementResult = {
  execution: rejectedExecution,
  output: "subject",
  outputBytes: 7,
  outputTruncated: false,
  truncated: false,
};

function runWithResult(
  expectation: RegexTestExpectation,
  execution: RegexExecutionResult,
  subject = "subject",
  replacementResult: RegexReplacementResult = {
    execution,
    output: subject,
    outputBytes: subject.length,
    outputTruncated: false,
    truncated: false,
  },
) {
  return runRegexTest(
    {
      id: "test",
      name: "Test",
      enabled: true,
      flavour: "ecmascript",
      pattern: "pattern",
      flags: [],
      options: {},
      subject,
      replacement: "replacement",
      expectation,
    },
    [],
    {
      execute: () => Promise.resolve(execution),
      replace: () => Promise.resolve(replacementResult),
    },
    2_000,
    10_000,
    100_000,
    64 * 1024 * 1024,
  );
}

describe("regex unit-test runner", () => {
  it("preserves explicit scan-all behavior in a saved test", async () => {
    let request: RegexExecutionRequest | undefined;
    await runRegexTest(
      {
        id: "scan-all",
        name: "Scan all",
        enabled: true,
        flavour: "ecmascript",
        flavourVersion: "ECMAScript fixture",
        pattern: "a",
        flags: [],
        options: { fixtureOption: true },
        scanAll: true,
        subject: "aa",
        expectation: { kind: "should-not-match" },
      },
      [],
      {
        execute: (candidate) => {
          request = candidate;
          return Promise.resolve(rejectedExecution);
        },
        replace: () => Promise.resolve(rejectedReplacement),
      },
      2_000,
      10_000,
      100_000,
      64 * 1024 * 1024,
    );

    expect(request?.scanAll).toBe(true);
    expect(request?.flavourVersion).toBe("ECMAScript fixture");
    expect(request?.options).toEqual({ fixtureOption: true });
  });

  it("never treats engine compile rejection as a successful no-match", async () => {
    const result = await runWithResult(
      { kind: "should-not-match" },
      rejectedExecution,
      "subject",
      rejectedReplacement,
    );

    expect(result).toEqual(
      expect.objectContaining({
        passed: false,
        timedOut: false,
        message:
          "Execution engine rejected the pattern: Unsupported regular-expression construct.",
      }),
    );
  });

  it("uses exact subject ranges instead of truncated value previews", async () => {
    const subject = "x".repeat(70_000);
    const preview = subject.slice(0, 64 * 1024);
    const execution: RegexExecutionResult = {
      ...rejectedExecution,
      accepted: true,
      diagnostics: [],
      matches: [
        {
          matchNumber: 1,
          value: preview,
          valueStatus: "truncated",
          range: { startUtf16: 0, endUtf16: subject.length },
          nativeRange: { start: 0, end: subject.length, unit: "utf16" },
          captures: [
            {
              groupNumber: 1,
              value: preview,
              status: "truncated",
              range: { startUtf16: 0, endUtf16: subject.length },
              nativeRange: { start: 0, end: subject.length, unit: "utf16" },
            },
          ],
        },
      ],
      truncated: false,
    };

    await expect(
      runWithResult(
        { kind: "full-match", matchIndex: 0, value: subject },
        execution,
        subject,
      ),
    ).resolves.toMatchObject({ passed: true });
    await expect(
      runWithResult(
        {
          kind: "capture",
          matchIndex: 0,
          groupNumber: 1,
          status: "participated",
          value: subject,
        },
        execution,
        subject,
      ),
    ).resolves.toMatchObject({ passed: true });
  });

  it("uses the participating capture for a duplicate named group", async () => {
    const execution: RegexExecutionResult = {
      ...rejectedExecution,
      accepted: true,
      diagnostics: [],
      matches: [
        {
          matchNumber: 1,
          value: "selected",
          valueStatus: "complete",
          range: { startUtf16: 0, endUtf16: 8 },
          nativeRange: { start: 0, end: 8, unit: "utf16" },
          captures: [
            {
              groupNumber: 1,
              groupName: "value",
              status: "did-not-participate",
            },
            {
              groupNumber: 2,
              groupName: "value",
              value: "selected",
              status: "participated",
              range: { startUtf16: 0, endUtf16: 8 },
              nativeRange: { start: 0, end: 8, unit: "utf16" },
            },
          ],
        },
      ],
      truncated: false,
    };

    await expect(
      runWithResult(
        {
          kind: "capture",
          matchIndex: 0,
          groupName: "value",
          status: "participated",
          value: "selected",
        },
        execution,
        "selected",
      ),
    ).resolves.toMatchObject({ passed: true });
  });

  it("does not assert complete counts from truncated match collection", async () => {
    const execution: RegexExecutionResult = {
      ...rejectedExecution,
      accepted: true,
      diagnostics: [],
      matches: [
        {
          matchNumber: 1,
          value: "x",
          valueStatus: "complete",
          range: { startUtf16: 0, endUtf16: 1 },
          nativeRange: { start: 0, end: 1, unit: "utf16" },
          captures: [],
        },
      ],
      truncated: true,
    };

    await expect(
      runWithResult({ kind: "match-count", count: 1 }, execution, "x"),
    ).resolves.toMatchObject({
      passed: false,
      message: expect.stringContaining("complete result set"),
    });
    await expect(
      runWithResult({ kind: "should-match" }, execution, "x"),
    ).resolves.toMatchObject({ passed: true });
  });

  it("does not assert equality from truncated replacement output", async () => {
    const execution: RegexExecutionResult = {
      ...rejectedExecution,
      accepted: true,
      diagnostics: [],
    };
    const replacement: RegexReplacementResult = {
      execution,
      output: "prefix",
      outputBytes: 6,
      outputTruncated: true,
      truncated: true,
    };

    await expect(
      runWithResult(
        { kind: "replacement", expected: "prefix" },
        execution,
        "subject",
        replacement,
      ),
    ).resolves.toMatchObject({
      passed: false,
      message: expect.stringContaining("truncated"),
    });
  });

  it("does not assert replacement equality from truncated match collection", async () => {
    const execution: RegexExecutionResult = {
      ...rejectedExecution,
      accepted: true,
      diagnostics: [],
      matches: [
        {
          matchNumber: 1,
          value: "a",
          valueStatus: "complete",
          range: { startUtf16: 0, endUtf16: 1 },
          nativeRange: { start: 0, end: 1, unit: "utf16" },
          captures: [],
        },
      ],
      truncated: true,
    };
    const replacement: RegexReplacementResult = {
      execution,
      output: "xa",
      outputBytes: 2,
      outputTruncated: false,
      truncated: true,
    };

    await expect(
      runWithResult(
        { kind: "replacement", expected: "xa" },
        execution,
        "aa",
        replacement,
      ),
    ).resolves.toMatchObject({
      passed: false,
      message: expect.stringContaining("match collection was truncated"),
    });
  });

  it("bounds retained mismatch diagnostics without stringifying complete values", async () => {
    const expected = `${"expected".repeat(200_000)}expected-tail`;
    const actual = `${"actual".repeat(300_000)}actual-tail`;
    const execution: RegexExecutionResult = {
      ...rejectedExecution,
      accepted: true,
      diagnostics: [],
    };
    const replacement: RegexReplacementResult = {
      execution,
      output: actual,
      outputBytes: actual.length,
      outputTruncated: false,
      truncated: false,
    };

    const result = await runWithResult(
      { kind: "replacement", expected },
      execution,
      "subject",
      replacement,
    );

    expect(result.passed).toBe(false);
    expect(new TextEncoder().encode(result.message).length).toBeLessThanOrEqual(
      MAXIMUM_REGEX_TEST_RESULT_MESSAGE_BYTES,
    );
    expect(result.message).toContain("diagnostic preview");
    expect(result.message).not.toContain("expected-tail");
    expect(result.message).not.toContain("actual-tail");
  });
});
