import { describe, expect, it } from "vitest";
import type { MinimizationCandidateObservation } from "./minimization.types";
import {
  containsLoneSurrogate,
  reduceSubject,
  unicodeScalars,
} from "./subject-transforms";

function observation(
  reproduced: boolean,
  overrides: Partial<MinimizationCandidateObservation> = {},
): MinimizationCandidateObservation {
  return {
    status: "complete",
    reproduced,
    fingerprint: reproduced ? ["fixture:failure"] : [],
    summary: reproduced ? "Fixture failure reproduced." : "Fixture passed.",
    ...overrides,
  };
}

const BUDGETS = {
  maximumEvaluations: 200,
  maximumWallTimeMs: 10_000,
  candidateTimeoutMs: 100,
} as const;

describe("deterministic subject transforms", () => {
  it("reduces left-to-right and reports only transform-local minimality", async () => {
    const candidates: string[] = [];
    const result = await reduceSubject({
      subject: "abXcd",
      targetIdentity: "fixture",
      budgets: BUDGETS,
      evaluate: async (candidate) => {
        candidates.push(candidate);
        return observation(candidate.includes("X"));
      },
      now: () => 0,
    });

    expect(result.minimizedSubject).toBe("X");
    expect(result).toMatchObject({
      status: "complete",
      stopReason: "locally-minimal",
      locallyMinimal: true,
      baseline: { reproduced: true },
      final: { reproduced: true },
    });
    expect(result.retainedHistory[0]?.kind).toBe("chunk-deletion");

    const repeated: string[] = [];
    await reduceSubject({
      subject: "abXcd",
      targetIdentity: "fixture",
      budgets: BUDGETS,
      evaluate: async (candidate) => {
        repeated.push(candidate);
        return observation(candidate.includes("X"));
      },
      now: () => 0,
    });
    expect(repeated).toEqual(candidates);
  });

  it("never splits supplementary Unicode scalars", async () => {
    const seen: string[] = [];
    const result = await reduceSubject({
      subject: "a😀b",
      targetIdentity: "unicode",
      budgets: BUDGETS,
      evaluate: async (candidate) => {
        seen.push(candidate);
        return observation(candidate.includes("😀"));
      },
      now: () => 0,
    });

    expect(result.minimizedSubject).toBe("😀");
    expect(seen.every((candidate) => !containsLoneSurrogate(candidate))).toBe(
      true,
    );
    expect(unicodeScalars(result.minimizedSubject)).toEqual(["😀"]);
  });

  it("uses only lower-rank canonical scalar simplifications", async () => {
    const result = await reduceSubject({
      subject: "Z",
      targetIdentity: "canonical",
      budgets: BUDGETS,
      evaluate: async (candidate) => observation(candidate.length > 0),
      now: () => 0,
    });

    expect(result.minimizedSubject).toBe("a");
    expect(result.retainedHistory).toContainEqual(
      expect.objectContaining({
        kind: "scalar-simplification",
        detail: 'Replaced scalar 0 with "a".',
      }),
    );
  });

  it("stops without a minimality claim when the evaluation budget is exhausted", async () => {
    const result = await reduceSubject({
      subject: "XX",
      targetIdentity: "budget",
      budgets: { ...BUDGETS, maximumEvaluations: 2 },
      evaluate: async (candidate) => observation(candidate.includes("X")),
      now: () => 0,
    });

    expect(result).toMatchObject({
      status: "partial",
      stopReason: "evaluation-budget-exhausted",
      locallyMinimal: false,
      evaluations: 2,
    });
  });

  it("reserves a full fixed candidate timeout inside the wall budget", async () => {
    let clock = 0;
    const result = await reduceSubject({
      subject: "XX",
      targetIdentity: "wall",
      budgets: {
        maximumEvaluations: 20,
        maximumWallTimeMs: 20,
        candidateTimeoutMs: 6,
      },
      evaluate: async (candidate) => {
        clock += 10;
        return observation(candidate.includes("X"));
      },
      now: () => clock,
    });

    expect(result).toMatchObject({
      status: "partial",
      stopReason: "wall-time-budget-exhausted",
      locallyMinimal: false,
      evaluations: 2,
    });
  });

  it("stops on a crash instead of treating it as a timeout or a reproduced failure", async () => {
    let calls = 0;
    const result = await reduceSubject({
      subject: "XX",
      targetIdentity: "crash",
      budgets: BUDGETS,
      evaluate: async () => {
        calls += 1;
        return calls === 1
          ? observation(true)
          : observation(false, {
              status: "crash",
              fingerprint: ["worker:crash"],
              summary: "Fixture worker crashed.",
            });
      },
      now: () => 0,
    });

    expect(result).toMatchObject({
      status: "unsupported",
      stopReason: "engine-failure",
      locallyMinimal: false,
      minimizedSubject: "XX",
      final: { status: "complete", reproduced: true },
    });
  });

  it("does not claim local minimality after an inconclusive candidate", async () => {
    let calls = 0;
    const result = await reduceSubject({
      subject: "XX",
      targetIdentity: "inconclusive",
      budgets: BUDGETS,
      evaluate: async () => {
        calls += 1;
        return calls === 1
          ? observation(true)
          : observation(false, {
              status: "inconclusive",
              fingerprint: ["truncated-result"],
              summary: "Fixture output was truncated.",
            });
      },
      now: () => 0,
    });

    expect(result).toMatchObject({
      status: "partial",
      stopReason: "inconclusive-candidate",
      locallyMinimal: false,
      evaluations: 2,
      minimizedSubject: "XX",
    });
  });

  it("returns a structured cancellation result", async () => {
    let cancelled = false;
    const progress: string[] = [];
    const result = await reduceSubject({
      subject: "XX",
      targetIdentity: "cancel",
      budgets: BUDGETS,
      evaluate: async () => {
        cancelled = true;
        return observation(true);
      },
      isCancelled: () => cancelled,
      onProgress: (value) => progress.push(value.phase),
      now: () => 0,
    });

    expect(result).toMatchObject({
      status: "cancelled",
      stopReason: "cancelled",
      locallyMinimal: false,
    });
    expect(progress).toContain("complete");
  });

  it("rejects lone surrogates before evaluating any candidate", async () => {
    let called = false;
    await expect(
      reduceSubject({
        subject: "\ud800",
        targetIdentity: "invalid-unicode",
        budgets: BUDGETS,
        evaluate: async () => {
          called = true;
          return observation(true);
        },
        now: () => 0,
      }),
    ).rejects.toThrow(/lone surrogate/u);
    expect(called).toBe(false);
  });
});
