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 } from "../model/syntax";
import { DEFAULT_REGEX_LIMITS } from "../execution/request-limits";
import { WorkerRequestError } from "../execution/WorkerSupervisor";
import {
  SubjectMinimizer,
  validateSubjectMinimizationRequest,
  type MinimizerEngineRunner,
  type MinimizerSyntaxRunner,
} from "./SubjectMinimizer";
import {
  MINIMIZER_MAXIMUM_SUBJECT_BYTES,
  type SubjectMinimizationRequest,
} from "./minimization.types";

const ECMASCRIPT_VERSION = "ECMAScript 2025 syntax / current browser runtime";
const PCRE2_VERSION = "PCRE2 10.47 8-bit WebAssembly";

function engineInfo(flavour: "ecmascript" | "pcre2"): RegexEngineInfo {
  return {
    flavour,
    adapterVersion: "fixture",
    engineName: flavour === "ecmascript" ? "Fixture JS" : "Fixture PCRE2",
    engineVersion: flavour === "ecmascript" ? "browser-1" : "10.47",
    offsetUnit: flavour === "ecmascript" ? "utf16" : "utf8-byte",
    capabilities: {
      compilation: true,
      matching: true,
      replacement: true,
      namedCaptures: true,
      captureHistory: false,
      actualTrace: false,
      benchmark: false,
    },
  };
}

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",
      children: [],
      properties: {
        zeroWidth: false,
        nullable: "unknown",
        consumesInput: "conditional",
      },
      support: {
        flavour: request.flavour,
        status: "supported",
        notes: [],
      },
      provenance: {
        provider: "fixture",
        providerVersion: "1",
        source: "parsed",
      },
    },
    tokens: [],
    captures: [],
    diagnostics: [],
    provider: { id: "fixture", version: "1" },
    coverage: {
      status: "full-tested",
      summary: "Fixture",
      unsupportedConstructs: [],
    },
  };
}

function syntaxRunner(): MinimizerSyntaxRunner {
  return {
    parsePattern: async (request) => syntax(request),
    cancel: vi.fn(),
    dispose: vi.fn(),
  };
}

function execution(
  request: RegexExecutionRequest,
  value?: string,
): RegexExecutionResult {
  const start = request.subject.indexOf("X");
  const hasMatch = value !== undefined;
  const rangeStart = Math.max(0, start);
  return {
    accepted: true,
    engine: engineInfo(request.flavour as "ecmascript" | "pcre2"),
    flags: {
      userFlags: request.flags.join(""),
      effectiveFlags: request.flags.join(""),
      internallyAddedIndicesFlag: request.flavour === "ecmascript",
      internallyAddedGlobalFlag: false,
    },
    matches: hasMatch
      ? [
          {
            matchNumber: 1,
            value,
            valueStatus: "complete",
            range: { startUtf16: rangeStart, endUtf16: rangeStart + 1 },
            nativeRange: {
              start: rangeStart,
              end: rangeStart + 1,
              unit: request.flavour === "pcre2" ? "utf8-byte" : "utf16",
            },
            captures: [],
          },
        ]
      : [],
    diagnostics: [],
    elapsedMs: 1,
    truncated: false,
  };
}

function engineRunner(
  execute: (
    request: RegexExecutionRequest,
    timeoutMs: number,
  ) => Promise<RegexExecutionResult>,
): MinimizerEngineRunner {
  return {
    execute,
    replace: async (
      request: RegexReplacementRequest,
      timeoutMs: number,
    ): Promise<RegexReplacementResult> => {
      const result = await execute(request, timeoutMs);
      return {
        execution: result,
        output: request.subject.replaceAll("X", request.replacement),
        outputBytes: request.subject.length,
        outputTruncated: false,
        truncated: false,
      };
    },
    cancel: vi.fn(),
    dispose: vi.fn(),
  };
}

function side(flavour: "ecmascript" | "pcre2") {
  return {
    flavour,
    flavourVersion:
      flavour === "ecmascript" ? ECMASCRIPT_VERSION : PCRE2_VERSION,
    pattern: "X",
    flags: ["g"],
    options: {},
  } as const;
}

function baseRequest(
  target: SubjectMinimizationRequest["target"],
  subject = "abXcd",
): SubjectMinimizationRequest {
  return {
    schemaVersion: 1,
    subject,
    target,
    budgets: {
      maximumEvaluations: 200,
      maximumWallTimeMs: 10_000,
      candidateTimeoutMs: 100,
    },
    maximumMatches: 100,
    maximumCaptureRows: 1_000,
    maximumOutputBytes: 4_096,
  };
}

function minimizer(runner: MinimizerEngineRunner): SubjectMinimizer {
  return new SubjectMinimizer({
    createSyntaxRunner: () => syntaxRunner(),
    createEngineRunner: () => runner,
    now: () => 0,
  });
}

describe("SubjectMinimizer real-run orchestration", () => {
  it("minimizes a failing unit-test subject against the selected engine", async () => {
    const runner = engineRunner(async (request) =>
      execution(request, request.subject.includes("X") ? "X" : undefined),
    );
    const instance = minimizer(runner);
    const progress: string[] = [];

    const result = await instance.minimize(
      baseRequest({
        kind: "engine",
        side: side("ecmascript"),
        scanAll: true,
        oracle: {
          kind: "unit-test-failure",
          expectation: { kind: "should-not-match" },
        },
      }),
      (value) => progress.push(value.phase),
    );

    expect(result).toMatchObject({
      status: "complete",
      stopReason: "locally-minimal",
      minimizedSubject: "X",
      baseline: {
        reproduced: true,
        fingerprint: ["expected-no-match:present"],
      },
      final: {
        reproduced: true,
        fingerprint: ["expected-no-match:present"],
      },
    });
    expect(result.baseline.sides?.[0]?.engineIdentity).toMatch(/Fixture JS/u);
    expect(progress).toContain("syntax-setup");
    expect(progress).toContain("local-sweep");
    instance.dispose();
  });

  it("preserves an exact engine timeout and never relabels a crash as one", async () => {
    const timeoutRunner = engineRunner(async (request) => {
      if (request.subject.includes("X")) {
        throw new WorkerRequestError("timeout", "fixture deadline");
      }
      return execution(request);
    });
    const timeoutMinimizer = minimizer(timeoutRunner);
    const timeoutResult = await timeoutMinimizer.minimize(
      baseRequest({
        kind: "engine",
        side: side("ecmascript"),
        scanAll: false,
        oracle: { kind: "engine-timeout" },
      }),
    );

    expect(timeoutResult).toMatchObject({
      status: "complete",
      minimizedSubject: "X",
      baseline: { status: "timeout", reproduced: true },
      final: { status: "timeout", reproduced: true },
    });
    timeoutMinimizer.dispose();

    const crashRunner = engineRunner(async () => {
      throw new WorkerRequestError("crash", "fixture worker crashed");
    });
    const crashMinimizer = minimizer(crashRunner);
    const crashResult = await crashMinimizer.minimize(
      baseRequest({
        kind: "engine",
        side: side("ecmascript"),
        scanAll: false,
        oracle: { kind: "engine-timeout" },
      }),
    );

    expect(crashResult).toMatchObject({
      status: "unsupported",
      stopReason: "engine-failure",
      locallyMinimal: false,
      baseline: { status: "crash", reproduced: false },
    });
    expect(crashResult.baseline.summary).toMatch(/not treated as a timeout/u);
    crashMinimizer.dispose();
  });

  it("preserves the exact semantic comparison mismatch category", async () => {
    const runner = engineRunner(async (request) => {
      if (!request.subject) return execution(request);
      if (request.flavour === "ecmascript") {
        return execution(request, request.subject.includes("X") ? "X" : "a");
      }
      return request.subject.includes("X")
        ? execution(request, "!")
        : execution(request);
    });
    const instance = minimizer(runner);

    const result = await instance.minimize(
      baseRequest(
        {
          kind: "comparison-mismatch",
          operation: "match",
          scanAll: true,
          sides: [side("ecmascript"), side("pcre2")],
        },
        "aX",
      ),
    );

    expect(result.minimizedSubject).toBe("X");
    expect(result.baseline).toMatchObject({
      reproduced: true,
      mismatchKinds: ["match-value"],
      fingerprint: ["comparison:semantic-mismatch", "match-value"],
    });
    expect(result.final.mismatchKinds).toEqual(["match-value"]);
    expect(result.locallyMinimal).toBe(true);
    instance.dispose();
  });

  it("preserves which comparison side timed out while its peer completes authoritatively", async () => {
    const runner = engineRunner(async (request) => {
      if (request.flavour === "pcre2" && request.subject.includes("X")) {
        throw new WorkerRequestError("timeout", "PCRE2 fixture deadline");
      }
      return execution(request);
    });
    const instance = minimizer(runner);

    const result = await instance.minimize(
      baseRequest({
        kind: "comparison-mismatch",
        operation: "match",
        scanAll: true,
        sides: [side("ecmascript"), side("pcre2")],
      }),
    );

    expect(result).toMatchObject({
      status: "complete",
      minimizedSubject: "X",
      baseline: {
        status: "timeout",
        reproduced: true,
        fingerprint: ["comparison:timeout:pcre2"],
      },
      final: {
        status: "timeout",
        reproduced: true,
        fingerprint: ["comparison:timeout:pcre2"],
      },
    });
    instance.dispose();
  });

  it("terminates active workers and returns cancellation progress", async () => {
    let rejectActive: ((reason: WorkerRequestError) => void) | undefined;
    let signalStarted: (() => void) | undefined;
    const started = new Promise<void>((resolve) => {
      signalStarted = resolve;
    });
    const runner = engineRunner(
      async () =>
        new Promise<RegexExecutionResult>((_resolve, reject) => {
          rejectActive = reject;
          signalStarted?.();
        }),
    );
    runner.cancel = vi.fn(() => {
      rejectActive?.(
        new WorkerRequestError("cancelled", "fixture request cancelled"),
      );
    });
    const instance = minimizer(runner);
    const running = instance.minimize(
      baseRequest({
        kind: "engine",
        side: side("ecmascript"),
        scanAll: false,
        oracle: { kind: "engine-timeout" },
      }),
    );

    await started;
    instance.cancel();
    const result = await running;

    expect(runner.cancel).toHaveBeenCalled();
    expect(result).toMatchObject({
      status: "cancelled",
      stopReason: "cancelled",
      locallyMinimal: false,
    });
    instance.dispose();
  });

  it("fails closed when the baseline result is truncated", async () => {
    const runner = engineRunner(async (request) => ({
      ...execution(request, "X"),
      truncated: true,
    }));
    const instance = minimizer(runner);

    const result = await instance.minimize(
      baseRequest({
        kind: "engine",
        side: side("ecmascript"),
        scanAll: true,
        oracle: {
          kind: "unit-test-failure",
          expectation: { kind: "should-not-match" },
        },
      }),
    );

    expect(result).toMatchObject({
      status: "unsupported",
      stopReason: "baseline-not-reproduced",
      evaluations: 1,
      baseline: {
        status: "inconclusive",
        reproduced: false,
        fingerprint: ["truncated-result"],
      },
    });
    instance.dispose();
  });
});

describe("subject-minimization request bounds", () => {
  const request = () =>
    baseRequest({
      kind: "engine",
      side: side("ecmascript"),
      scanAll: false,
      oracle: { kind: "engine-timeout" },
    });

  it("enforces evaluation, aggregate wall and fixed candidate budgets", () => {
    expect(() =>
      validateSubjectMinimizationRequest({
        ...request(),
        budgets: {
          maximumEvaluations: DEFAULT_REGEX_LIMITS.maximumMinimizerRuns + 1,
          maximumWallTimeMs: 1_000,
          candidateTimeoutMs: 100,
        },
      }),
    ).toThrow(/evaluation budget/u);
    expect(() =>
      validateSubjectMinimizationRequest({
        ...request(),
        budgets: {
          maximumEvaluations: 10,
          maximumWallTimeMs: 100,
          candidateTimeoutMs: 101,
        },
      }),
    ).toThrow(/must fit inside/u);
  });

  it("rejects oversized or malformed Unicode subjects before worker setup", () => {
    expect(() =>
      validateSubjectMinimizationRequest({
        ...request(),
        subject: "a".repeat(MINIMIZER_MAXIMUM_SUBJECT_BYTES + 1),
      }),
    ).toThrow(/1,048,576 UTF-8 bytes/u);
    expect(() =>
      validateSubjectMinimizationRequest({
        ...request(),
        subject: "\ud800",
      }),
    ).toThrow(/lone surrogate/u);
  });
});
