import { describe, expect, it } from "vitest";
import { WorkerRequestError } from "../execution/WorkerSupervisor";
import { EcmaScriptEngineAdapter } from "../execution/adapters/ecmascript/EcmaScriptEngineAdapter";
import type { RegexEngineInfo } from "../model/flavour";
import type {
  RegexExecutionRequest,
  RegexExecutionResult,
} from "../model/match";
import type { NormalizedRegexNode } from "../model/syntax";
import { CaseGenerationOrchestrator } from "./CaseGenerationOrchestrator";
import { DEFAULT_GENERATION_SETTINGS } from "./generation-limits";
import { generateCandidateSubjects } from "./generate-candidates";
import type {
  CandidateGenerationReport,
  CaseGenerationInput,
  GeneratedSubjectCandidate,
} from "./generation.types";
import type { GenerationWorkerClient } from "./GenerationSupervisor";
import { EcmaScriptSyntaxProvider } from "../syntax/providers/ecmascript/EcmaScriptSyntaxProvider";

const ENGINE: RegexEngineInfo = {
  flavour: "ecmascript",
  adapterVersion: "fixture-1",
  engineName: "Native ECMAScript RegExp",
  engineVersion: "fixture",
  runtimeVersion: "test",
  offsetUnit: "utf16",
  capabilities: {
    compilation: true,
    matching: true,
    replacement: true,
    namedCaptures: true,
    captureHistory: false,
    actualTrace: false,
    benchmark: true,
  },
};

function root(): NormalizedRegexNode {
  return {
    id: "root",
    kind: "pattern",
    range: { startUtf16: 0, endUtf16: 3 },
    raw: "yes",
    explanation: "fixture",
    children: [],
    properties: {
      zeroWidth: false,
      nullable: false,
      minimumLength: 3,
      maximumLength: 3,
      consumesInput: true,
    },
    support: { flavour: "ecmascript", status: "supported", notes: [] },
    provenance: {
      provider: "fixture",
      providerVersion: "1",
      source: "parsed",
    },
  };
}

function candidate(
  id: string,
  subject: string,
  intendedOutcome: "match" | "no-match",
): GeneratedSubjectCandidate {
  return {
    id,
    intendedOutcome,
    subject,
    subjectBytes: new TextEncoder().encode(subject).byteLength,
    label: `candidate ${id}`,
    features: [intendedOutcome === "match" ? "shortest" : "likely-near-miss"],
    notes: [],
  };
}

function report(
  candidates: readonly GeneratedSubjectCandidate[],
): CandidateGenerationReport {
  return {
    generator: { id: "regex-tools-ast-cases", version: "1" },
    flavour: "ecmascript",
    seed: "orchestrator-seed",
    seedHash: "12345678",
    candidates,
    coverage: [
      {
        feature: "shortest",
        status: "covered",
        detail: "fixture",
        relevantNodes: 1,
      },
    ],
    unsupportedConstructs: [],
    visitedAstNodes: 1,
    candidateBytes: candidates.reduce(
      (total, item) => total + item.subjectBytes,
      0,
    ),
    candidateAttempts: candidates.length,
    duplicateCandidates: 0,
    candidatesTruncated: false,
    traversalTruncated: false,
    warnings: [],
  };
}

class FakeGenerator implements GenerationWorkerClient {
  cancelled = false;
  disposed = false;
  readonly value:
    CandidateGenerationReport | Promise<CandidateGenerationReport>;

  constructor(
    value: CandidateGenerationReport | Promise<CandidateGenerationReport>,
  ) {
    this.value = value;
  }

  generate(): Promise<CandidateGenerationReport> {
    return Promise.resolve(this.value);
  }

  cancel(): void {
    this.cancelled = true;
  }

  dispose(): void {
    this.disposed = true;
  }
}

function execution(
  request: RegexExecutionRequest,
  {
    accepted = true,
    matched = request.subject.includes("yes"),
  }: { readonly accepted?: boolean; readonly matched?: boolean } = {},
): RegexExecutionResult {
  return {
    accepted,
    engine: ENGINE,
    flags: {
      userFlags: request.flags.join(""),
      effectiveFlags: request.flags.join(""),
      internallyAddedIndicesFlag: false,
      internallyAddedGlobalFlag: false,
    },
    matches: matched
      ? [
          {
            matchNumber: 1,
            value: "yes",
            valueStatus: "complete",
            range: { startUtf16: 0, endUtf16: 3 },
            nativeRange: { start: 0, end: 3, unit: "utf16" },
            captures: [],
          },
        ]
      : [],
    diagnostics: accepted
      ? []
      : [
          {
            id: "compile",
            source: "execution-engine",
            severity: "error",
            code: "compile-error",
            message: "fixture compile rejection",
            flavour: "ecmascript",
            provenance: "reported",
          },
        ],
    elapsedMs: 0.25,
    truncated: false,
  };
}

function input(
  settings: Partial<CaseGenerationInput["settings"]> = {},
): CaseGenerationInput {
  return {
    flavour: "ecmascript",
    flavourVersion: "2025",
    pattern: "yes",
    flags: [],
    options: {},
    scanAll: false,
    root: root(),
    captureMetadata: [],
    syntaxAccepted: true,
    seed: "orchestrator-seed",
    settings: { ...DEFAULT_GENERATION_SETTINGS, ...settings },
  };
}

describe("CaseGenerationOrchestrator", () => {
  it("retains only candidates confirmed by the selected actual engine", async () => {
    const generator = new FakeGenerator(
      report([
        candidate("positive", "yes", "match"),
        candidate("negative", "no", "no-match"),
        candidate("false-positive", "no", "match"),
        candidate("false-negative", "yes", "no-match"),
      ]),
    );
    const requests: RegexExecutionRequest[] = [];
    const engine = {
      execute: async (request: RegexExecutionRequest) => {
        requests.push(request);
        return execution(request);
      },
      cancel: () => undefined,
      dispose: () => undefined,
    };
    const progress: string[] = [];
    const orchestrator = new CaseGenerationOrchestrator({
      generator,
      engine,
    });

    const result = await orchestrator.run(input(), (value) =>
      progress.push(value.phase),
    );

    expect(result.cases.map((item) => item.expectation)).toEqual([
      "should-match",
      "should-not-match",
    ]);
    expect(result.cases[0]?.provenance).toEqual(
      expect.objectContaining({
        kind: "generated",
        seed: "orchestrator-seed",
        candidateId: "positive",
      }),
    );
    expect(result.discarded.map((item) => item.reason)).toEqual([
      "unexpected-no-match",
      "unexpected-match",
    ]);
    expect(result.engine).toEqual(ENGINE);
    expect(result.engineExecutions).toBe(4);
    expect(requests.every((request) => request.flavour === "ecmascript")).toBe(
      true,
    );
    expect(progress).toContain("synthesizing");
    expect(progress).toContain("verifying");
  });

  it("keeps timeout, crash and compile rejection distinct", async () => {
    const candidates = [
      candidate("timeout", "timeout", "match"),
      candidate("crash", "crash", "match"),
      candidate("compile", "yes", "match"),
    ];
    const engine = {
      execute: async (request: RegexExecutionRequest) => {
        if (request.subject === "timeout") {
          throw new WorkerRequestError("timeout", "fixture timeout");
        }
        if (request.subject === "crash") {
          throw new WorkerRequestError("crash", "fixture crash");
        }
        return execution(request, { accepted: false });
      },
      cancel: () => undefined,
      dispose: () => undefined,
    };
    const orchestrator = new CaseGenerationOrchestrator({
      generator: new FakeGenerator(report(candidates)),
      engine,
    });

    const result = await orchestrator.run(input());

    expect(result.status).toBe("compile-rejected");
    expect(result.discarded.map((item) => item.reason)).toEqual([
      "timeout",
      "worker-crash",
      "compile-rejected",
    ]);
    expect(result.stoppedReason).toContain("fixture compile rejection");
  });

  it("honours the retained-case cap and aggregate wall clock", async () => {
    const candidates = [
      candidate("one", "yes", "match"),
      candidate("two", "yes yes", "match"),
    ];
    const engine = {
      execute: async (request: RegexExecutionRequest) => execution(request),
      cancel: () => undefined,
      dispose: () => undefined,
    };
    const retained = new CaseGenerationOrchestrator({
      generator: new FakeGenerator(report(candidates)),
      engine,
    });
    const retainedResult = await retained.run(
      input({ maximumCases: 1, maximumCandidateAttempts: 4 }),
    );
    expect(retainedResult.status).toBe("partial");
    expect(retainedResult.cases).toHaveLength(1);
    expect(retainedResult.stoppedReason).toContain("maximum of 1");

    let now = 0;
    const wall = new CaseGenerationOrchestrator({
      generator: new FakeGenerator(report(candidates)),
      engine,
      now: () => {
        now += 6;
        return now;
      },
    });
    const wallResult = await wall.run(
      input({
        maximumWallTimeMs: 10,
        maximumCases: 2,
        maximumCandidateAttempts: 4,
      }),
    );
    expect(wallResult.status).toBe("wall-time-limit");
    expect(wallResult.stoppedReason).toContain("aggregate wall-time");
  });

  it("cancels both synthesis and engine workers and rejects stale work", async () => {
    let rejectGeneration: ((error: Error) => void) | undefined;
    const pending = new Promise<CandidateGenerationReport>((_, reject) => {
      rejectGeneration = reject;
    });
    const generator = new FakeGenerator(pending);
    const engine = {
      execute: async (request: RegexExecutionRequest) => execution(request),
      cancelled: false,
      disposed: false,
      cancel() {
        this.cancelled = true;
      },
      dispose() {
        this.disposed = true;
      },
    };
    const orchestrator = new CaseGenerationOrchestrator({
      generator,
      engine,
    });
    const run = orchestrator.run(input());
    orchestrator.cancel();
    rejectGeneration?.(
      new WorkerRequestError("cancelled", "fixture cancellation"),
    );

    await expect(run).rejects.toMatchObject({ kind: "cancelled" });
    expect(generator.cancelled).toBe(true);
    expect(engine.cancelled).toBe(true);
    orchestrator.dispose();
    expect(generator.disposed).toBe(true);
    expect(engine.disposed).toBe(true);
  });

  it("does not invoke an engine for unsupported or rejected syntax", async () => {
    const empty = report([]);
    const generator = new FakeGenerator(empty);
    let executions = 0;
    const engine = {
      execute: async (request: RegexExecutionRequest) => {
        executions += 1;
        return execution(request);
      },
      cancel: () => undefined,
      dispose: () => undefined,
    };
    const orchestrator = new CaseGenerationOrchestrator({
      generator,
      engine,
    });

    const unsupported = await orchestrator.run(input());
    expect(unsupported.status).toBe("unsupported");
    expect(executions).toBe(0);

    const rejected = await orchestrator.run({
      ...input(),
      syntaxAccepted: false,
    });
    expect(rejected.status).toBe("unsupported");
    expect(executions).toBe(0);
  });

  it("verifies generated positives and negatives through the real ECMAScript adapter", async () => {
    const pattern = "^(cat|dog)s{0,2}\\p{Letter}$";
    const flags = ["u"];
    const syntax = await new EcmaScriptSyntaxProvider().parsePattern({
      flavour: "ecmascript",
      flavourVersion: "2025",
      pattern,
      flags,
      options: {},
    });
    const adapter = new EcmaScriptEngineAdapter();
    const orchestrator = new CaseGenerationOrchestrator({
      generator: {
        generate: async (candidateRequest) =>
          generateCandidateSubjects(candidateRequest),
        cancel: () => undefined,
        dispose: () => undefined,
      },
      engine: {
        execute: async (executionRequest) => adapter.execute(executionRequest),
        cancel: () => undefined,
        dispose: () => undefined,
      },
    });

    const result = await orchestrator.run({
      flavour: "ecmascript",
      flavourVersion: "2025",
      pattern,
      flags,
      options: {},
      scanAll: false,
      root: syntax.root,
      captureMetadata: syntax.captures,
      syntaxAccepted: syntax.accepted,
      seed: "real-adapter-seed",
      settings: {
        ...DEFAULT_GENERATION_SETTINGS,
        maximumCases: 2,
        maximumCandidateAttempts: 96,
        randomVariantCount: 8,
      },
    });

    expect(result.engine?.engineName).toBe("Native ECMAScript RegExp");
    expect(
      result.cases.some(
        (candidate) => candidate.expectation === "should-match",
      ),
    ).toBe(true);
    expect(
      result.cases.some(
        (candidate) => candidate.expectation === "should-not-match",
      ),
    ).toBe(true);
    expect(result.cases).toHaveLength(2);
    const expression = new RegExp(pattern, "u");
    for (const candidate of result.cases) {
      expect(expression.test(candidate.subject)).toBe(
        candidate.expectation === "should-match",
      );
    }
  });
});
