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 { AnalysisWorkerClient } from "../regex/analysis/AnalysisSupervisor";
import type {
  BenchmarkWorkerSample,
  GrowthWorkerSample,
} from "../regex/analysis/analysis.types";
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
import { EcmaScriptSyntaxProvider } from "../regex/syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
import { AnalysisPanel } from "./AnalysisPanel";

const provider = new EcmaScriptSyntaxProvider();

function benchmarkSample(milliseconds = 1): BenchmarkWorkerSample {
  return {
    accepted: true,
    effectiveFlags: "dg",
    subjectBytes: 5,
    subjectUtf16: 5,
    compileMs: milliseconds,
    firstMatchMs: milliseconds + 1,
    allMatchesMs: milliseconds + 2,
    replacementMs: milliseconds + 3,
    throughputBytesPerSecond: 1024 * 1024,
    matchCount: 1,
    matched: true,
    matchCollectionTruncated: false,
    replacementOutputUtf16: 1,
  };
}

function growthSample(executionMs: number): GrowthWorkerSample {
  return {
    accepted: true,
    effectiveFlags: "d",
    subjectBytes: 5,
    subjectUtf16: 5,
    executionMs,
    matchCount: 0,
    matched: false,
    matchCollectionTruncated: false,
  };
}

function worker(
  overrides: Partial<AnalysisWorkerClient> = {},
): AnalysisWorkerClient {
  return {
    identity: vi.fn().mockResolvedValue({
      flavour: "ecmascript",
      engineName: "Native ECMAScript RegExp",
      engineVersion: "Fixture Browser 1",
      runtimeVersion: "Fixture Browser 1",
      nativeOffsetUnit: "utf16",
    }),
    benchmarkSample: vi.fn().mockResolvedValue(benchmarkSample()),
    growthProbe: vi.fn().mockResolvedValue(growthSample(1)),
    cancel: vi.fn(),
    dispose: vi.fn(),
    ...overrides,
  };
}

async function renderPanel(
  overrides: Partial<Parameters<typeof AnalysisPanel>[0]> = {},
) {
  const pattern = overrides.pattern ?? "(a+)+$";
  const flavour = overrides.flavour ?? "ecmascript";
  const syntax =
    overrides.syntax ??
    (await provider.parsePattern({
      flavour: "ecmascript",
      flavourVersion: "2025",
      pattern,
      flags: [],
      options: {},
    }));
  const currentWorker = worker();
  const rendered = render(
    <AnalysisPanel
      active
      flavour={flavour}
      pattern={pattern}
      flags={[]}
      subject="aaaa!"
      replacement="x"
      scanAll={false}
      syntax={syntax}
      createSupervisor={() => currentWorker}
      {...overrides}
    />,
  );
  return { ...rendered, currentWorker, syntax };
}

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

describe("AnalysisPanel", () => {
  it("shows flavour-scoped advisory static findings and selects their source range", async () => {
    const onSelectPatternRange = vi.fn();
    await renderPanel({ onSelectPatternRange });

    expect(
      screen.getByRole("heading", { name: "Potential risk findings" }),
    ).toBeInTheDocument();
    expect(screen.getByText("Potential nested-quantifier risk")).toBeVisible();
    expect(screen.getByText(/potential risks found/u)).toBeVisible();
    await userEvent.click(
      screen.getByRole("button", {
        name: /Potential nested-quantifier risk/u,
      }),
    );
    expect(onSelectPatternRange).toHaveBeenCalledWith({
      startUtf16: 0,
      endUtf16: 5,
    });
  });

  it("runs bounded cold and warm benchmark samples and renders p95", async () => {
    const { currentWorker } = await renderPanel();
    await userEvent.click(screen.getByRole("tab", { name: "Benchmark" }));
    await userEvent.clear(
      screen.getByRole("spinbutton", { name: "Measured samples" }),
    );
    await userEvent.type(
      screen.getByRole("spinbutton", { name: "Measured samples" }),
      "3",
    );
    await userEvent.click(
      screen.getByRole("button", { name: "Run bounded benchmark" }),
    );

    const table = await screen.findByRole("table", {
      name: "Cold and warm benchmark metrics",
    });
    expect(table).toHaveTextContent("Warm p95");
    expect(table).toHaveTextContent("Compile");
    expect(screen.getByText(/Fixture Browser 1/u)).toBeVisible();
    expect(screen.getAllByText(/3 measured/u)).toHaveLength(2);
    expect(currentWorker.identity).toHaveBeenCalled();
    expect(currentWorker.benchmarkSample).toHaveBeenCalledTimes(7);
  });

  it("renders separate timing and outcome plots for bounded growth", async () => {
    const samples = [growthSample(0.5), growthSample(10)];
    const currentWorker = worker({
      growthProbe: vi
        .fn()
        .mockImplementation(() => Promise.resolve(samples.shift()!)),
    });
    await renderPanel({ createSupervisor: () => currentWorker });
    await userEvent.click(
      screen.getByRole("button", { name: "Run bounded growth probe" }),
    );

    expect(
      await screen.findByRole("img", {
        name: /Execution-time growth plot/u,
      }),
    ).toBeVisible();
    expect(
      screen.getByRole("img", { name: /Outcome plot with 2 samples/u }),
    ).toBeVisible();
    expect(screen.getByText("Observed disproportionate growth")).toBeVisible();
    expect(
      screen.getByRole("table", { name: "Dynamic growth samples" }),
    ).toHaveTextContent("Normalized growth");
  });

  it("does not apply ECMAScript analysis to PCRE2", async () => {
    const currentWorker = worker();
    await renderPanel({
      flavour: "pcre2",
      createSupervisor: () => currentWorker,
    });

    expect(screen.getByRole("alert")).toHaveTextContent(
      "supports ECMAScript 2025 only",
    );
    expect(
      screen.getByRole("button", { name: "Run bounded growth probe" }),
    ).toBeDisabled();
    expect(currentWorker.identity).not.toHaveBeenCalled();
  });

  it("never renders benchmark results against a changed subject", async () => {
    const rendered = await renderPanel();
    await userEvent.click(screen.getByRole("tab", { name: "Benchmark" }));
    await userEvent.click(
      screen.getByRole("button", { name: "Run bounded benchmark" }),
    );
    expect(
      await screen.findByRole("table", {
        name: "Cold and warm benchmark metrics",
      }),
    ).toBeVisible();

    rendered.rerender(
      <AnalysisPanel
        active
        flavour="ecmascript"
        pattern="(a+)+$"
        flags={[]}
        subject="changed subject"
        replacement="x"
        scanAll={false}
        syntax={rendered.syntax}
        createSupervisor={() => rendered.currentWorker}
      />,
    );

    expect(
      screen.queryByRole("table", {
        name: "Cold and warm benchmark metrics",
      }),
    ).not.toBeInTheDocument();
  });

  it("terminates the active worker when the user cancels", async () => {
    let rejectSample: ((reason: Error) => void) | undefined;
    const pending = new Promise<BenchmarkWorkerSample>((_resolve, reject) => {
      rejectSample = reject;
    });
    const currentWorker = worker({
      benchmarkSample: vi.fn().mockReturnValue(pending),
      cancel: vi.fn().mockImplementation(() => {
        rejectSample?.(
          new WorkerRequestError("cancelled", "fixture cancellation"),
        );
      }),
    });
    await renderPanel({ createSupervisor: () => currentWorker });
    await userEvent.click(screen.getByRole("tab", { name: "Benchmark" }));
    await userEvent.click(
      screen.getByRole("button", { name: "Run bounded benchmark" }),
    );
    await userEvent.click(
      await screen.findByRole("button", { name: "Cancel benchmark" }),
    );

    await waitFor(() => expect(currentWorker.cancel).toHaveBeenCalled());
    expect(await screen.findAllByText(/Benchmark cancelled/u)).toHaveLength(2);
    expect(currentWorker.dispose).toHaveBeenCalled();
  });
});
