import {
  cleanup,
  fireEvent,
  render,
  screen,
  waitFor,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type {
  RegexTestCase,
  RegexTestResult,
} from "../regex/tests/test-case.types";
import {
  UnitTestPanel,
  type RegexTestDraft,
  type UnitTestPanelProps,
} from "./UnitTestPanel";

afterEach(cleanup);

function renderPanel(
  overrides: Partial<UnitTestPanelProps> = {},
): UnitTestPanelProps {
  const props: UnitTestPanelProps = {
    tests: [],
    results: new Map<string, RegexTestResult>(),
    currentFlavour: "ecmascript",
    currentPattern: "current-pattern",
    currentFlags: ["u"],
    currentScanAll: false,
    currentSubject: "current subject",
    currentReplacement: "current replacement",
    onAdd: vi.fn(),
    onUpdate: vi.fn(),
    onClone: vi.fn(),
    onImport: vi.fn(),
    onRemove: vi.fn(),
    onToggle: vi.fn(),
    onRunAll: vi.fn(),
    onRunSelected: vi.fn(),
    onCancel: vi.fn(),
    running: false,
    ...overrides,
  };
  render(<UnitTestPanel {...props} />);
  return props;
}

async function replaceInput(
  user: ReturnType<typeof userEvent.setup>,
  element: HTMLElement,
  value: string,
): Promise<void> {
  await user.clear(element);
  await user.type(element, value);
}

function submittedDrafts(props: UnitTestPanelProps): readonly RegexTestDraft[] {
  return vi.mocked(props.onAdd).mock.calls.map(([draft]) => draft);
}

describe("UnitTestPanel", () => {
  it("creates both presence assertions from the current subject", async () => {
    const user = userEvent.setup();
    const props = renderPanel();

    await user.click(screen.getByRole("button", { name: "Add test" }));
    await user.selectOptions(
      screen.getByRole("combobox", { name: "Expectation" }),
      "should-not-match",
    );
    await user.click(screen.getByRole("button", { name: "Add test" }));

    expect(submittedDrafts(props)).toEqual([
      {
        name: "Current example",
        pattern: "current-pattern",
        flags: ["u"],
        scanAll: false,
        subject: "current subject",
        expectation: { kind: "should-match" },
      },
      {
        name: "Current example",
        pattern: "current-pattern",
        flags: ["u"],
        scanAll: false,
        subject: "current subject",
        expectation: { kind: "should-not-match" },
      },
    ]);
  });

  it("creates count, full-match, completion, and timeout assertions", async () => {
    const user = userEvent.setup();
    const props = renderPanel();
    const expectation = screen.getByRole("combobox", { name: "Expectation" });

    await user.selectOptions(expectation, "match-count");
    await replaceInput(
      user,
      screen.getByRole("spinbutton", { name: "Expected match count" }),
      "3",
    );
    await user.click(screen.getByRole("button", { name: "Add test" }));

    await user.selectOptions(expectation, "full-match");
    await replaceInput(
      user,
      screen.getByRole("spinbutton", {
        name: "Match index (zero-based)",
      }),
      "2",
    );
    await user.type(
      screen.getByRole("textbox", { name: "Expected full match" }),
      "three",
    );
    await user.click(screen.getByRole("button", { name: "Add test" }));

    await user.selectOptions(expectation, "must-complete-within");
    await replaceInput(
      user,
      screen.getByRole("spinbutton", { name: "Completion limit (ms)" }),
      "75",
    );
    await user.click(screen.getByRole("button", { name: "Add test" }));

    await user.selectOptions(expectation, "must-time-out");
    await user.click(screen.getByRole("button", { name: "Add test" }));

    expect(submittedDrafts(props).map((draft) => draft.expectation)).toEqual([
      { kind: "match-count", count: 3 },
      { kind: "full-match", matchIndex: 2, value: "three" },
      { kind: "must-complete-within", milliseconds: 75 },
      { kind: "must-time-out" },
    ]);
  });

  it("creates capture assertions by number and by name with optional value", async () => {
    const user = userEvent.setup();
    const props = renderPanel();

    await user.selectOptions(
      screen.getByRole("combobox", { name: "Expectation" }),
      "capture",
    );
    await replaceInput(
      user,
      screen.getByRole("spinbutton", { name: "Capture number" }),
      "4",
    );
    await user.selectOptions(
      screen.getByRole("combobox", { name: "Expected capture status" }),
      "matched-empty",
    );
    await user.click(screen.getByRole("button", { name: "Add test" }));

    await user.selectOptions(
      screen.getByRole("combobox", { name: "Identify capture by" }),
      "name",
    );
    await user.type(
      screen.getByRole("textbox", { name: "Capture name" }),
      "word",
    );
    await user.selectOptions(
      screen.getByRole("combobox", { name: "Expected capture status" }),
      "participated",
    );
    await user.click(
      screen.getByRole("checkbox", {
        name: "Also assert the exact capture value",
      }),
    );
    await user.type(
      screen.getByRole("textbox", { name: "Expected capture value" }),
      "hello",
    );
    await user.click(screen.getByRole("button", { name: "Add test" }));

    expect(submittedDrafts(props).map((draft) => draft.expectation)).toEqual([
      {
        kind: "capture",
        matchIndex: 0,
        groupNumber: 4,
        status: "matched-empty",
      },
      {
        kind: "capture",
        matchIndex: 0,
        groupName: "word",
        status: "participated",
        value: "hello",
      },
    ]);
  });

  it("creates replacement assertions with optional template and custom timeout", async () => {
    const user = userEvent.setup();
    const props = renderPanel();

    await user.selectOptions(
      screen.getByRole("combobox", { name: "Expectation" }),
      "replacement",
    );
    await user.type(
      screen.getByRole("textbox", { name: "Expected replacement output" }),
      "Hello Ada",
    );
    await replaceInput(
      user,
      screen.getByRole("spinbutton", {
        name: "Per-test timeout (ms, optional)",
      }),
      "750",
    );
    await user.click(screen.getByRole("button", { name: "Add test" }));

    await user.click(
      screen.getByRole("checkbox", {
        name: "Store a replacement template for this test",
      }),
    );
    await replaceInput(
      user,
      screen.getByRole("textbox", { name: "Replacement template" }),
      "Hello $<name>",
    );
    await user.click(screen.getByRole("button", { name: "Add test" }));

    expect(submittedDrafts(props)).toEqual([
      {
        name: "Current example",
        pattern: "current-pattern",
        flags: ["u"],
        scanAll: false,
        subject: "current subject",
        expectation: {
          kind: "replacement",
          expected: "Hello Ada",
        },
        resourceLimits: {
          manualExecutionTimeoutMs: 750,
        },
      },
      {
        name: "Current example",
        pattern: "current-pattern",
        flags: ["u"],
        scanAll: false,
        subject: "current subject",
        expectation: {
          kind: "replacement",
          expected: "Hello Ada",
        },
        replacement: "Hello $<name>",
        resourceLimits: {
          manualExecutionTimeoutMs: 750,
        },
      },
    ]);
  });

  it("reports invalid assertion and timeout bounds without adding a test", async () => {
    const user = userEvent.setup();
    const props = renderPanel();

    await user.selectOptions(
      screen.getByRole("combobox", { name: "Expectation" }),
      "match-count",
    );
    await replaceInput(
      user,
      screen.getByRole("spinbutton", { name: "Expected match count" }),
      "1000001",
    );
    await user.click(screen.getByRole("button", { name: "Add test" }));

    expect(screen.getByRole("alert")).toHaveTextContent(
      "Expected match count must be a whole number",
    );
    expect(props.onAdd).not.toHaveBeenCalled();

    await replaceInput(
      user,
      screen.getByRole("spinbutton", { name: "Expected match count" }),
      "1",
    );
    await replaceInput(
      user,
      screen.getByRole("spinbutton", {
        name: "Per-test timeout (ms, optional)",
      }),
      "10001",
    );
    await user.click(screen.getByRole("button", { name: "Add test" }));

    expect(screen.getByRole("alert")).toHaveTextContent(
      "Per-test timeout must be a whole number",
    );
    expect(props.onAdd).not.toHaveBeenCalled();
  });

  it("rejects an astral subject above the execution UTF-8 byte cap", () => {
    const props = renderPanel();
    const astralSubject = "😀".repeat(4 * 1024 * 1024 + 1);

    fireEvent.change(screen.getByRole("textbox", { name: "Subject" }), {
      target: { value: astralSubject },
    });
    fireEvent.submit(
      screen.getByRole("textbox", { name: "Name" }).closest("form")!,
    );

    expect(screen.getByRole("alert")).toHaveTextContent(
      "16,777,216 UTF-8 byte limit",
    );
    expect(props.onAdd).not.toHaveBeenCalled();
  });

  it("preserves suite run, toggle, remove, and result behavior", async () => {
    const user = userEvent.setup();
    const test: RegexTestCase = {
      id: "test-1",
      name: "Greeting",
      enabled: true,
      flavour: "ecmascript",
      pattern: "hello",
      flags: ["i"],
      options: {},
      subject: "Hello",
      expectation: { kind: "match-count", count: 1 },
    };
    const result: RegexTestResult = {
      testId: test.id,
      passed: true,
      elapsedMs: 1,
      message: "Matched once.",
      timedOut: false,
    };
    const props = renderPanel({
      tests: [test],
      results: new Map([[test.id, result]]),
    });

    expect(screen.getByText("match count: 1", { exact: false })).toBeVisible();
    expect(screen.getByRole("status")).toHaveTextContent(
      "Pass — Matched once.",
    );

    await user.click(screen.getByRole("checkbox", { name: "Enable Greeting" }));
    await user.click(screen.getByRole("button", { name: "Remove Greeting" }));
    await user.click(screen.getByRole("button", { name: "Run all" }));

    expect(props.onToggle).toHaveBeenCalledWith("test-1");
    expect(props.onRemove).toHaveBeenCalledWith("test-1");
    expect(props.onRunAll).toHaveBeenCalledOnce();
  });

  it("locks suite mutations but keeps cancellation available while running", async () => {
    const user = userEvent.setup();
    const test: RegexTestCase = {
      id: "test-1",
      name: "Snapshot case",
      enabled: true,
      flavour: "ecmascript",
      pattern: "snapshot",
      flags: [],
      options: {},
      subject: "snapshot",
      expectation: { kind: "should-match" },
    };
    const props = renderPanel({
      tests: [test],
      running: true,
    });

    expect(screen.getByRole("button", { name: "Add test" })).toBeDisabled();
    expect(
      screen.getByRole("checkbox", { name: "Enable Snapshot case" }),
    ).toBeDisabled();
    expect(
      screen.getByRole("button", { name: "Remove Snapshot case" }),
    ).toBeDisabled();
    expect(
      screen.queryByRole("button", { name: "Run all" }),
    ).not.toBeInTheDocument();

    const cancel = screen.getByRole("button", { name: "Cancel" });
    expect(cancel).toBeEnabled();
    fireEvent.submit(
      screen.getByRole("textbox", { name: "Name" }).closest("form")!,
    );
    await user.click(cancel);

    expect(props.onCancel).toHaveBeenCalledOnce();
    expect(props.onAdd).not.toHaveBeenCalled();
    expect(props.onToggle).not.toHaveBeenCalled();
    expect(props.onRemove).not.toHaveBeenCalled();
  });

  it("edits, clones, selects, reruns, and filters suite cases", async () => {
    const user = userEvent.setup();
    const failing: RegexTestCase = {
      id: "failing",
      name: "Failing case",
      enabled: true,
      flavour: "ecmascript",
      pattern: "old",
      flags: ["i"],
      options: {},
      scanAll: true,
      subject: "subject",
      expectation: { kind: "should-match" },
    };
    const passing: RegexTestCase = {
      ...failing,
      id: "passing",
      name: "Passing case",
    };
    const props = renderPanel({
      tests: [failing, passing],
      results: new Map([
        [
          failing.id,
          {
            testId: failing.id,
            passed: false,
            elapsedMs: 1,
            message: "Expected at least one match.",
            timedOut: false,
          },
        ],
        [
          passing.id,
          {
            testId: passing.id,
            passed: true,
            elapsedMs: 1,
            message: "Matched once.",
            timedOut: false,
          },
        ],
      ]),
    });

    await user.click(
      screen.getByRole("checkbox", { name: "Select Failing case" }),
    );
    await user.click(screen.getByRole("button", { name: "Run selected" }));
    expect(props.onRunSelected).toHaveBeenCalledWith(["failing"]);

    await user.click(
      screen.getByRole("button", { name: "Clone Failing case" }),
    );
    expect(props.onClone).toHaveBeenCalledWith("failing");

    await user.click(screen.getByRole("button", { name: "Edit Failing case" }));
    expect(
      screen.getByRole("heading", { name: "Edit unit test" }),
    ).toBeVisible();
    await replaceInput(
      user,
      screen.getByRole("textbox", { name: "Name" }),
      "Updated case",
    );
    await replaceInput(
      user,
      screen.getByRole("textbox", { name: "Pattern" }),
      "new",
    );
    await user.click(screen.getByRole("button", { name: "Update test" }));

    expect(props.onUpdate).toHaveBeenCalledWith(
      "failing",
      expect.objectContaining({
        name: "Updated case",
        pattern: "new",
        flags: ["i"],
        scanAll: true,
        subject: "subject",
        expectation: { kind: "should-match" },
      }),
    );

    await user.click(screen.getByRole("checkbox", { name: "Failures only" }));
    expect(screen.getByText("Failing case")).toBeVisible();
    expect(screen.queryByText("Passing case")).not.toBeInTheDocument();
  });

  it("shows generated seed provenance and removes it from an edited draft", async () => {
    const user = userEvent.setup();
    const generated: RegexTestCase = {
      id: "generated",
      name: "Generated positive",
      enabled: true,
      flavour: "ecmascript",
      pattern: "cat",
      flags: [],
      options: {},
      subject: "cat",
      expectation: { kind: "should-match" },
      generation: {
        kind: "generated",
        generatorId: "regex-tools-ast-cases",
        generatorVersion: "1",
        seed: "browser-seed-42",
        candidateId: "generated-aabbccdd-1",
        intendedOutcome: "match",
      },
    };
    const props = renderPanel({ tests: [generated] });

    expect(screen.getByText(/generated · seed browser-seed-42/u)).toBeVisible();
    await user.click(
      screen.getByRole("button", { name: "Edit Generated positive" }),
    );
    await user.click(screen.getByRole("button", { name: "Update test" }));

    expect(props.onUpdate).toHaveBeenCalledWith(
      "generated",
      expect.not.objectContaining({ generation: expect.anything() }),
    );
  });

  it("adds a bounded snapshot of the latest current result", async () => {
    const user = userEvent.setup();
    const currentResultDraft: RegexTestDraft = {
      name: "Current match result",
      pattern: "word",
      flags: ["g"],
      scanAll: true,
      subject: "word word",
      expectation: { kind: "match-count", count: 2 },
    };
    const props = renderPanel({ currentResultDraft });

    await user.click(
      screen.getByRole("button", { name: "Add current result" }),
    );

    expect(props.onAdd).toHaveBeenCalledWith(currentResultDraft);
  });

  it("reports an aggregate state-limit rejection from cloning", async () => {
    const user = userEvent.setup();
    const test: RegexTestCase = {
      id: "large",
      name: "Large case",
      enabled: true,
      flavour: "ecmascript",
      pattern: "large",
      flags: [],
      options: {},
      subject: "large",
      expectation: { kind: "should-match" },
    };
    renderPanel({
      tests: [test],
      onClone: vi.fn(() => {
        throw new Error("Test suite exceeds the 32 MiB aggregate state limit.");
      }),
    });

    await user.click(screen.getByRole("button", { name: "Clone Large case" }));

    expect(screen.getByRole("alert")).toHaveTextContent(
      "32 MiB aggregate state limit",
    );
  });

  it("imports a validated standalone JSON suite", async () => {
    const imported: RegexTestCase = {
      id: "imported",
      name: "Imported case",
      enabled: true,
      flavour: "ecmascript",
      pattern: "imported",
      flags: [],
      options: {},
      subject: "imported",
      expectation: { kind: "should-match" },
    };
    const serialized = JSON.stringify({
      schemaVersion: 1,
      tests: [imported],
    });
    const file = new File([serialized], "suite.json", {
      type: "application/json",
    });
    Object.defineProperty(file, "text", {
      value: () => Promise.resolve(serialized),
    });
    const props = renderPanel();
    const input = screen.getByLabelText("Import test-suite JSON file");
    expect(input).toHaveAttribute("tabindex", "-1");

    fireEvent.change(input, {
      target: { files: [file] },
    });

    await waitFor(() =>
      expect(props.onImport).toHaveBeenCalledWith([imported]),
    );
    expect(screen.getByRole("status")).toHaveTextContent("Imported 1 test.");
  });
});
