import { describe, expect, it } from "vitest";
import type { NormalizedRegexNode } from "../model/syntax";
import { EcmaScriptSyntaxProvider } from "../syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
import { DEFAULT_GENERATION_SETTINGS } from "./generation-limits";
import { generateCandidateSubjects } from "./generate-candidates";
import type {
  CandidateGenerationRequest,
  CandidateGenerationSettings,
} from "./generation.types";

const provider = new EcmaScriptSyntaxProvider();

async function request(
  pattern: string,
  {
    seed = "fixture-seed",
    flags = ["u"],
    settings = {},
  }: {
    readonly seed?: string;
    readonly flags?: readonly string[];
    readonly settings?: Partial<CandidateGenerationSettings>;
  } = {},
): Promise<CandidateGenerationRequest> {
  const syntax = await provider.parsePattern({
    flavour: "ecmascript",
    pattern,
    flags,
    options: {},
  });
  expect(syntax.accepted).toBe(true);
  return {
    flavour: "ecmascript",
    root: syntax.root,
    flags,
    seed,
    settings: { ...DEFAULT_GENERATION_SETTINGS, ...settings },
  };
}

describe("generateCandidateSubjects", () => {
  it("covers alternatives, optional boundaries, classes and Unicode representatives", async () => {
    const report = generateCandidateSubjects(
      await request("(cat|dog)s{0,2}\\p{Letter}", {
        settings: { randomVariantCount: 4 },
      }),
    );

    expect(
      report.candidates.some((candidate) => candidate.subject === "catA"),
    ).toBe(true);
    expect(
      report.candidates.some((candidate) => candidate.subject === "dogA"),
    ).toBe(true);
    expect(
      report.candidates.some(
        (candidate) =>
          candidate.features.includes("quantifier-boundary") &&
          candidate.subject.includes("ss"),
      ),
    ).toBe(true);
    expect(
      report.candidates.some((candidate) => candidate.subject.includes("é")),
    ).toBe(true);
    expect(
      report.coverage.find((entry) => entry.feature === "alternative-coverage")
        ?.status,
    ).toBe("covered");
    expect(
      report.coverage.find((entry) => entry.feature === "optional-presence")
        ?.status,
    ).toBe("covered");
    expect(report.seed).toBe("fixture-seed");
    expect(report.seedHash).toMatch(/^[\da-f]{8}$/u);
    expect(
      report.candidates
        .slice(0, 2)
        .map((candidate) => candidate.intendedOutcome),
    ).toEqual(["match", "no-match"]);
  });

  it("is byte-for-byte deterministic for a seed and changes seeded variants for another seed", async () => {
    const source = "(a|b|c)(x|y|z){1,4}[0-9]";
    const first = generateCandidateSubjects(
      await request(source, {
        seed: "alpha",
        settings: { randomVariantCount: 12 },
      }),
    );
    const replay = generateCandidateSubjects(
      await request(source, {
        seed: "alpha",
        settings: { randomVariantCount: 12 },
      }),
    );
    const second = generateCandidateSubjects(
      await request(source, {
        seed: "bravo",
        settings: { randomVariantCount: 12 },
      }),
    );

    expect(replay).toEqual(first);
    expect(
      first.candidates
        .filter((candidate) => candidate.features.includes("seeded-random"))
        .map((candidate) => candidate.subject),
    ).not.toEqual(
      second.candidates
        .filter((candidate) => candidate.features.includes("seeded-random"))
        .map((candidate) => candidate.subject),
    );
  });

  it("reports unsupported stateful constructs without claiming complete coverage", async () => {
    const report = generateCandidateSubjects(
      await request("(a)\\1(?=x)x", { flags: [] }),
    );

    expect(report.unsupportedConstructs).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ kind: "backreference" }),
        expect.objectContaining({ kind: "lookahead" }),
      ]),
    );
    expect(
      report.coverage.find((entry) => entry.feature === "shortest")?.status,
    ).toBe("partial");
  });

  it("does not claim structural enumeration for Unicode properties of strings", async () => {
    const report = generateCandidateSubjects(
      await request("\\p{RGI_Emoji}", { flags: ["v"] }),
    );

    expect(report.unsupportedConstructs).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          kind: "unicode-property",
          reason: expect.stringContaining("multiple code points"),
        }),
      ]),
    );
    expect(
      report.coverage.find(
        (entry) => entry.feature === "unicode-representative",
      )?.status,
    ).toBe("partial");
  });

  it("refuses to reinterpret a partial PCRE2 AST as ECMAScript", () => {
    const root: NormalizedRegexNode = {
      id: "pcre-root",
      kind: "pattern",
      range: { startUtf16: 0, endUtf16: 6 },
      raw: "(?R)",
      explanation: "PCRE2 pattern",
      children: [],
      properties: {
        zeroWidth: false,
        nullable: "unknown",
        consumesInput: "conditional",
      },
      support: {
        flavour: "pcre2",
        status: "partial",
        notes: [],
      },
      provenance: {
        provider: "pcre2-lexical",
        providerVersion: "1",
        source: "parsed",
      },
    };
    const report = generateCandidateSubjects({
      flavour: "pcre2",
      root,
      flags: [],
      seed: "pcre-seed",
      settings: DEFAULT_GENERATION_SETTINGS,
    });

    expect(report.candidates).toHaveLength(0);
    expect(
      report.coverage.every((entry) => entry.status === "unsupported"),
    ).toBe(true);
    expect(report.stoppedReason).toContain("not reinterpreted");
  });

  it("enforces traversal, attempt and byte caps before verification", async () => {
    const attemptLimited = generateCandidateSubjects(
      await request("(a|b|c|d)", {
        settings: {
          maximumCases: 3,
          maximumCandidateAttempts: 3,
          randomVariantCount: 0,
        },
      }),
    );
    expect(attemptLimited.candidateAttempts).toBe(3);
    expect(attemptLimited.candidatesTruncated).toBe(true);
    expect(attemptLimited.stoppedReason).toContain("attempt limit");

    const byteLimited = generateCandidateSubjects(
      await request("abcdef", {
        settings: {
          maximumSubjectBytes: 3,
          maximumTotalSubjectBytes: 3,
          randomVariantCount: 0,
        },
      }),
    );
    expect(byteLimited.candidates).toHaveLength(0);
    expect(byteLimited.warnings.join(" ")).toContain("per-subject limit");

    const nodeLimited = generateCandidateSubjects(
      await request("abcdefgh", {
        settings: {
          maximumAstNodes: 2,
          randomVariantCount: 0,
        },
      }),
    );
    expect(nodeLimited.traversalTruncated).toBe(true);
    expect(nodeLimited.candidates).toHaveLength(0);
  });

  it("validates every user-controlled bound and the explicit seed", async () => {
    const base = await request("a");
    expect(() => generateCandidateSubjects({ ...base, seed: "" })).toThrow(
      "Generation seed",
    );
    expect(() =>
      generateCandidateSubjects({
        ...base,
        settings: {
          ...base.settings,
          maximumCandidateAttempts: base.settings.maximumCases - 1,
        },
      }),
    ).toThrow("Maximum candidate attempts");
    expect(() =>
      generateCandidateSubjects({
        ...base,
        settings: {
          ...base.settings,
          maximumSubjectBytes: base.settings.maximumTotalSubjectBytes + 1,
        },
      }),
    ).toThrow("per-subject byte limit");
  });
});
