import { describe, expect, it, vi } from "vitest";
import type { RegexEngineInfo } from "../model/flavour";
import type {
  RegexExecutionRequest,
  RegexExecutionResult,
  RegexReplacementRequest,
  RegexReplacementResult,
} from "../model/match";
import type {
  RegexSyntaxRequest,
  RegexSyntaxResult,
  ReplacementSyntaxResult,
} from "../model/syntax";
import { WorkerRequestError } from "../execution/WorkerSupervisor";
import {
  ComparisonOrchestrator,
  type ComparisonEngineRunner,
  type ComparisonSyntaxRunner,
} from "./ComparisonOrchestrator";
import type {
  ComparisonFlavour,
  RegexComparisonRequest,
} from "./comparison.types";

function syntax(request: RegexSyntaxRequest): RegexSyntaxResult {
  return {
    accepted: true,
    root: {
      id: `${request.flavour}-root`,
      kind: "pattern",
      range: { startUtf16: 0, endUtf16: request.pattern.length },
      raw: request.pattern,
      explanation: "Fixture syntax",
      children: [],
      properties: {
        zeroWidth: false,
        nullable: "unknown",
        consumesInput: "conditional",
      },
      support: {
        flavour: request.flavour,
        status: "partial",
        notes: [],
      },
      provenance: {
        provider: `${request.flavour}-fixture`,
        providerVersion: "1",
        source: "parsed",
      },
    },
    tokens: [],
    captures: [
      {
        number: 1,
        name: "word",
        range: { startUtf16: 0, endUtf16: request.pattern.length },
        repeated: false,
      },
    ],
    diagnostics: [],
    provider: { id: `${request.flavour}-fixture`, version: "1" },
    coverage: {
      status: "partial",
      summary: "Fixture",
      unsupportedConstructs: [],
    },
  };
}

function engineInfo(flavour: ComparisonFlavour): RegexEngineInfo {
  return {
    flavour,
    adapterVersion: "fixture",
    engineName:
      flavour === "ecmascript" ? "Native ECMAScript RegExp" : "PCRE2 WASM",
    engineVersion: flavour === "ecmascript" ? "fixture-browser" : "10.47",
    offsetUnit: flavour === "ecmascript" ? "utf16" : "utf8-byte",
    capabilities: {
      compilation: true,
      matching: true,
      replacement: true,
      namedCaptures: true,
      captureHistory: false,
      actualTrace: false,
      benchmark: false,
    },
  };
}

function execution(request: RegexExecutionRequest): RegexExecutionResult {
  const nativeEnd = request.flavour === "pcre2" ? 5 : 4;
  return {
    accepted: true,
    engine: engineInfo(request.flavour as ComparisonFlavour),
    flags: {
      userFlags: request.flags.join(""),
      effectiveFlags: request.flags.join(""),
      internallyAddedIndicesFlag: request.flavour === "ecmascript",
      internallyAddedGlobalFlag: false,
    },
    matches: [
      {
        matchNumber: 1,
        value: "café",
        valueStatus: "complete",
        range: { startUtf16: 0, endUtf16: 4 },
        nativeRange: {
          start: 0,
          end: nativeEnd,
          unit: request.flavour === "pcre2" ? "utf8-byte" : "utf16",
        },
        captures: [
          {
            groupNumber: 1,
            groupName: "word",
            value: "café",
            status: "participated",
            range: { startUtf16: 0, endUtf16: 4 },
            nativeRange: {
              start: 0,
              end: nativeEnd,
              unit: request.flavour === "pcre2" ? "utf8-byte" : "utf16",
            },
          },
        ],
      },
    ],
    diagnostics: [],
    elapsedMs: 1,
    truncated: false,
  };
}

function request(
  operation: "match" | "replace" = "match",
): RegexComparisonRequest {
  return {
    schemaVersion: 1,
    patternModel: "shared",
    operation,
    subject: "café",
    scanAll: true,
    maximumMatches: 100,
    maximumCaptureRows: 1_000,
    maximumOutputBytes: 1_024,
    timeoutMs: 500,
    sides: [
      {
        flavour: "ecmascript",
        flavourVersion: "ECMAScript 2025 syntax / current browser runtime",
        pattern: "(?<word>.+)",
        flags: ["g", "u"],
        options: {},
        ...(operation === "replace" ? { replacement: "$<word>!" } : {}),
      },
      {
        flavour: "pcre2",
        flavourVersion: "PCRE2 10.47 8-bit WebAssembly",
        pattern: "(?<word>.+)",
        flags: ["g"],
        options: {
          matchLimit: 1_000_000,
          depthLimit: 1_000,
          heapLimitKib: 32_768,
        },
        ...(operation === "replace" ? { replacement: "${word}!" } : {}),
      },
    ],
  };
}

function syntaxRunner(
  parsePattern = async (value: RegexSyntaxRequest) => syntax(value),
): ComparisonSyntaxRunner {
  return {
    parsePattern,
    parseReplacement: async (): Promise<ReplacementSyntaxResult> => ({
      accepted: true,
      tokens: [],
      diagnostics: [],
    }),
    cancel: vi.fn(),
    dispose: vi.fn(),
  };
}

function engineRunner(
  execute = async (value: RegexExecutionRequest) => execution(value),
): ComparisonEngineRunner {
  return {
    execute,
    replace: async (
      value: RegexReplacementRequest,
    ): Promise<RegexReplacementResult> => ({
      execution: await execute(value),
      output: `${value.subject}!`,
      outputBytes: value.subject.length + 1,
      outputTruncated: false,
      truncated: false,
    }),
    cancel: vi.fn(),
    dispose: vi.fn(),
  };
}

describe("ComparisonOrchestrator", () => {
  it("retains exact requests and aligns UTF-16 results while preserving native offsets", async () => {
    let now = 0;
    const runner = engineRunner();
    const orchestrator = new ComparisonOrchestrator({
      createSyntaxRunner: () => syntaxRunner(),
      createEngineRunner: () => runner,
      now: () => (now += 1),
      isoNow: () => "2026-07-26T12:00:00.000Z",
    });

    const result = await orchestrator.compare(request());

    expect(result.status).toBe("same-for-current-input");
    expect(result.notComparable).toEqual([]);
    expect(result.matchAlignments).toHaveLength(1);
    expect(result.matchAlignments[0]).toMatchObject({
      alignment: "normalized-range",
      equal: true,
      left: {
        nativeRange: { end: 4, unit: "utf16" },
      },
      right: {
        nativeRange: { end: 5, unit: "utf8-byte" },
      },
    });
    expect(result.differences.map((difference) => difference.kind)).toEqual([
      "effective-flags",
      "offset-model",
    ]);
    expect(result.sides[0].executionRequest).toMatchObject({
      flavour: "ecmascript",
      pattern: "(?<word>.+)",
      subject: "café",
      captureMetadata: [{ number: 1, name: "word" }],
    });
    expect(result.sides[1].requestIdentity).toMatch(/^request-[0-9a-f]{8}$/u);
    expect(result.notices[0]?.message).toMatch(/not a proof/u);
    orchestrator.dispose();
  });

  it("records an independent timeout instead of interpreting it as no match", async () => {
    const runner = engineRunner(async (value) => {
      if (value.flavour === "pcre2") {
        throw new WorkerRequestError("timeout", "PCRE2 exceeded 500 ms");
      }
      return execution(value);
    });
    const orchestrator = new ComparisonOrchestrator({
      createSyntaxRunner: () => syntaxRunner(),
      createEngineRunner: () => runner,
      now: () => 1,
      isoNow: () => "2026-07-26T12:00:00.000Z",
    });

    const result = await orchestrator.compare(request());

    expect(result.status).toBe("not-comparable");
    expect(result.sides[0].runtime.status).toBe("complete");
    expect(result.sides[1].runtime.status).toBe("timeout");
    expect(result.notComparable).toContainEqual(
      expect.objectContaining({
        code: "execution-timeout",
        flavour: "pcre2",
      }),
    );
    expect(result.notComparable[0]?.message).toMatch(/not a no-match/u);
    orchestrator.dispose();
  });

  it("keeps per-flavour replacement templates exact", async () => {
    const seen: RegexReplacementRequest[] = [];
    const runner = engineRunner();
    runner.replace = async (value) => {
      seen.push(value);
      return {
        execution: execution(value),
        output:
          value.flavour === "ecmascript"
            ? value.replacement
            : value.replacement,
        outputBytes: value.replacement.length,
        outputTruncated: false,
        truncated: false,
      };
    };
    const orchestrator = new ComparisonOrchestrator({
      createSyntaxRunner: () => syntaxRunner(),
      createEngineRunner: () => runner,
      now: () => 1,
      isoNow: () => "2026-07-26T12:00:00.000Z",
    });

    const result = await orchestrator.compare(request("replace"));

    expect(seen.map((value) => value.replacement)).toEqual([
      "$<word>!",
      "${word}!",
    ]);
    expect(result.status).toBe("different-for-current-input");
    expect(
      result.differences.find(
        (difference) => difference.kind === "replacement-output",
      ),
    ).toMatchObject({
      left: "$<word>!",
      right: "${word}!",
    });
    orchestrator.dispose();
  });

  it("refuses mismatched shared patterns before constructing a semantic result", async () => {
    const input = request();
    const invalid: RegexComparisonRequest = {
      ...input,
      sides: [input.sides[0], { ...input.sides[1], pattern: "(?<word>\\w+)" }],
    };
    const orchestrator = new ComparisonOrchestrator({
      createSyntaxRunner: () => syntaxRunner(),
      createEngineRunner: () => engineRunner(),
      now: () => 1,
      isoNow: () => "2026-07-26T12:00:00.000Z",
    });

    await expect(orchestrator.compare(invalid)).rejects.toThrow(
      /exact same pattern/u,
    );
    orchestrator.dispose();
  });
});
