import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { CaseGenerationClient } from "../regex/generation/CaseGenerationOrchestrator";
import type {
  CaseGenerationResult,
  VerifiedGeneratedCase,
} from "../regex/generation/generation.types";
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
import { EcmaScriptSyntaxProvider } from "../regex/syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
import { GenerationPanel } from "./GenerationPanel";

const provider = new EcmaScriptSyntaxProvider();

function generatedCase(
  id: string,
  expectation: "should-match" | "should-not-match",
): VerifiedGeneratedCase {
  const subject = expectation === "should-match" ? "cat" : "dog";
  return {
    id,
    name: `Generated ${id}`,
    subject,
    subjectBytes: subject.length,
    expectation,
    features:
      expectation === "should-match" ? ["shortest"] : ["likely-near-miss"],
    notes: [],
    elapsedMs: 0.5,
    matched: expectation === "should-match",
    matchCount: expectation === "should-match" ? 1 : 0,
    provenance: {
      kind: "generated",
      generatorId: "regex-tools-ast-cases",
      generatorVersion: "1",
      seed: "case-seed-1",
      candidateId: id,
      intendedOutcome: expectation === "should-match" ? "match" : "no-match",
    },
  };
}

function fixtureResult(): CaseGenerationResult {
  return {
    status: "partial",
    seed: "case-seed-1",
    seedHash: "1234abcd",
    flavour: "ecmascript",
    cases: [
      generatedCase("positive", "should-match"),
      generatedCase("negative", "should-not-match"),
    ],
    discarded: [
      {
        candidateId: "discarded",
        intendedOutcome: "no-match",
        reason: "unexpected-match",
        detail: "The actual engine matched it.",
        subjectBytes: 3,
        subjectPreview: "cat",
      },
    ],
    coverage: [
      {
        feature: "shortest",
        status: "covered",
        detail: "Shortest path requested.",
        relevantNodes: 1,
      },
      {
        feature: "alternative-coverage",
        status: "not-applicable",
        detail: "No alternatives.",
        relevantNodes: 0,
      },
    ],
    unsupportedConstructs: [
      {
        id: "unsupported-lookahead",
        kind: "lookahead",
        range: { startUtf16: 0, endUtf16: 3 },
        rawPreview: "(?=cat)",
        reason: "Lookaround is verified but not synthesized.",
      },
    ],
    engine: {
      flavour: "ecmascript",
      adapterVersion: "fixture",
      engineName: "Native ECMAScript RegExp",
      engineVersion: "Fixture Browser 1",
      runtimeVersion: "Fixture Browser 1",
      offsetUnit: "utf16",
      capabilities: {
        compilation: true,
        matching: true,
        replacement: true,
        namedCaptures: true,
        captureHistory: false,
        actualTrace: false,
        benchmark: true,
      },
    },
    candidateAttempts: 5,
    engineExecutions: 3,
    generatedCandidateBytes: 9,
    verifiedSubjectBytes: 6,
    wallTimeMs: 4.5,
    warnings: [],
  };
}

function client(
  overrides: Partial<CaseGenerationClient> = {},
): CaseGenerationClient {
  return {
    run: vi.fn().mockImplementation(async (_input, onProgress) => {
      onProgress?.({
        phase: "verifying",
        completed: 1,
        total: 3,
        retained: 1,
        message: "Verifying fixture candidate.",
      });
      return fixtureResult();
    }),
    cancel: vi.fn(),
    dispose: vi.fn(),
    ...overrides,
  };
}

async function renderPanel(
  overrides: Partial<Parameters<typeof GenerationPanel>[0]> = {},
) {
  const pattern = overrides.pattern ?? "cat";
  const syntax =
    overrides.syntax ??
    (await provider.parsePattern({
      flavour: "ecmascript",
      flavourVersion: "2025",
      pattern,
      flags: [],
      options: {},
    }));
  const currentClient = client();
  const rendered = render(
    <GenerationPanel
      active
      flavour="ecmascript"
      flavourVersion="2025"
      pattern={pattern}
      flags={[]}
      options={{}}
      scanAll={false}
      syntax={syntax}
      createOrchestrator={() => currentClient}
      {...overrides}
    />,
  );
  return { ...rendered, currentClient, syntax };
}

afterEach(() => {
  cleanup();
});

describe("GenerationPanel", () => {
  it("runs deterministic generation, renders honest coverage and adds verified cases to tests", async () => {
    const onAddToTests = vi.fn();
    const onSelectPatternRange = vi.fn();
    const { currentClient } = await renderPanel({
      onAddToTests,
      onSelectPatternRange,
    });

    await userEvent.click(
      screen.getByRole("button", { name: "Generate & verify" }),
    );

    expect(
      await screen.findByText(/Native ECMAScript RegExp · Fixture Browser 1/u),
    ).toBeVisible();
    expect(screen.getByText("case-seed-1")).toBeVisible();
    expect(
      screen.getByRole("region", { name: "Generation summary" }),
    ).toHaveTextContent("1234abcd");
    expect(screen.getByText("Shortest path requested.")).toBeVisible();
    expect(screen.getAllByText("actual engine verified")).toHaveLength(2);
    await userEvent.click(
      screen.getByText(/1 unsupported or partially synthesized construct/u),
    );
    expect(currentClient.run).toHaveBeenCalledWith(
      expect.objectContaining({
        seed: "case-seed-1",
        flavour: "ecmascript",
        pattern: "cat",
      }),
      expect.any(Function),
    );

    await userEvent.click(
      screen.getByRole("button", {
        name: /lookahead · 0–3/u,
      }),
    );
    expect(onSelectPatternRange).toHaveBeenCalledWith({
      startUtf16: 0,
      endUtf16: 3,
    });

    await userEvent.click(
      screen.getByText(/Advanced: 1 rejected candidate diagnostic/u),
    );
    expect(screen.getByText(/unexpected match/u)).toBeVisible();

    await userEvent.click(
      screen.getByRole("button", { name: "Add selected to tests" }),
    );
    expect(onAddToTests).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({
          expectation: "should-match",
          provenance: expect.objectContaining({ seed: "case-seed-1" }),
        }),
        expect.objectContaining({ expectation: "should-not-match" }),
      ]),
    );
    expect(
      screen.getByText(/Added 2 generated cases to the unit-test suite/u),
    ).toBeVisible();
  });

  it("shows strict setting errors without starting worker execution", async () => {
    const { currentClient } = await renderPanel();
    const maximumCases = screen.getByRole("spinbutton", {
      name: "Retain at most",
    });
    await userEvent.clear(maximumCases);
    await userEvent.type(maximumCases, "500");
    await userEvent.click(screen.getByText("Advanced generation bounds"));
    const attempts = screen.getByRole("spinbutton", {
      name: "Candidate attempts",
    });
    await userEvent.clear(attempts);
    await userEvent.type(attempts, "100");
    await userEvent.click(
      screen.getByRole("button", { name: "Generate & verify" }),
    );

    expect(
      await screen.findByText(/Maximum candidate attempts must be/u),
    ).toBeVisible();
    expect(currentClient.run).not.toHaveBeenCalled();
  });

  it("does not expose the ECMAScript generator for PCRE2", async () => {
    const { currentClient } = await renderPanel({ flavour: "pcre2" });

    expect(
      screen.getByText(
        /current PCRE2 syntax provider is intentionally partial/u,
      ),
    ).toBeVisible();
    expect(
      screen.queryByRole("button", { name: "Generate & verify" }),
    ).not.toBeInTheDocument();
    expect(currentClient.run).not.toHaveBeenCalled();
  });

  it("clears verified cases immediately when the pattern changes", async () => {
    const rendered = await renderPanel();
    await userEvent.click(
      screen.getByRole("button", { name: "Generate & verify" }),
    );
    expect(await screen.findByText("Generated positive")).toBeVisible();

    rendered.rerender(
      <GenerationPanel
        active
        flavour="ecmascript"
        flavourVersion="2025"
        pattern="changed"
        flags={[]}
        options={{}}
        scanAll={false}
        syntax={rendered.syntax}
        createOrchestrator={() => rendered.currentClient}
      />,
    );

    expect(screen.queryByText("Generated positive")).not.toBeInTheDocument();
    expect(
      await screen.findByText(/previous generated cases were cleared/u),
    ).toBeVisible();
  });

  it("terminates synthesis and engine workers on cancellation", async () => {
    let rejectRun: ((error: Error) => void) | undefined;
    const pending = new Promise<CaseGenerationResult>((_resolve, reject) => {
      rejectRun = reject;
    });
    const currentClient = client({
      run: vi.fn().mockReturnValue(pending),
      cancel: vi.fn().mockImplementation(() => {
        rejectRun?.(
          new WorkerRequestError("cancelled", "fixture cancellation"),
        );
      }),
    });
    await renderPanel({ createOrchestrator: () => currentClient });
    await userEvent.click(
      screen.getByRole("button", { name: "Generate & verify" }),
    );
    await userEvent.click(
      await screen.findByRole("button", { name: "Cancel generation" }),
    );

    await waitFor(() => expect(currentClient.cancel).toHaveBeenCalled());
    expect(
      await screen.findByText(
        /Generation cancelled; its workers were terminated/u,
      ),
    ).toBeVisible();
    expect(currentClient.dispose).toHaveBeenCalled();
  });
});
