import {
  act,
  cleanup,
  fireEvent,
  render,
  screen,
} from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { RegexExecutionRequest } from "../regex/model/match";
import type {
  CaptureDefinition,
  RegexSyntaxRequest,
  RegexSyntaxResult,
  ReplacementSyntaxResult,
} from "../regex/model/syntax";

interface PendingPattern {
  readonly request: RegexSyntaxRequest;
  readonly resolve: (result: RegexSyntaxResult) => void;
}

interface PendingReplacement {
  readonly replacement: string;
  readonly resolve: (result: ReplacementSyntaxResult) => void;
}

interface SyntaxInstance {
  readonly label: string;
  cancelCalls: number;
}

const syntaxHarness = vi.hoisted(() => ({
  instances: [] as SyntaxInstance[],
  patterns: [] as PendingPattern[],
  replacements: [] as PendingReplacement[],
}));

const engineHarness = vi.hoisted(() => ({
  requests: [] as RegexExecutionRequest[],
  emitWholeFirstCharacterMatch: false,
}));

vi.mock("../editors/CodeEditor", () => ({
  CodeEditor({
    value,
    onChange,
    label,
    readOnly,
    marks,
    selectedRange,
    onSelectionChange,
  }: {
    readonly value: string;
    readonly onChange?: (value: string) => void;
    readonly label: string;
    readonly readOnly?: boolean;
    readonly marks?: readonly unknown[];
    readonly selectedRange?: { readonly startUtf16: number };
    readonly onSelectionChange?: (range: {
      readonly startUtf16: number;
      readonly endUtf16: number;
    }) => void;
  }) {
    return (
      <textarea
        aria-label={label}
        value={value}
        readOnly={readOnly}
        data-mark-count={marks?.length ?? 0}
        data-selected-start={selectedRange?.startUtf16}
        onChange={(event) => onChange?.(event.target.value)}
        onSelect={(event) =>
          onSelectionChange?.({
            startUtf16: event.currentTarget.selectionStart,
            endUtf16: event.currentTarget.selectionEnd,
          })
        }
      />
    );
  },
}));

vi.mock("../regex/execution/SyntaxSupervisor", () => ({
  SyntaxSupervisor: class {
    readonly instance: SyntaxInstance;

    constructor(label = "Syntax parser") {
      this.instance = { label, cancelCalls: 0 };
      syntaxHarness.instances.push(this.instance);
    }

    parsePattern(request: RegexSyntaxRequest): Promise<RegexSyntaxResult> {
      return new Promise((resolve) => {
        syntaxHarness.patterns.push({ request, resolve });
      });
    }

    parseReplacement(request: {
      readonly replacement: string;
    }): Promise<ReplacementSyntaxResult> {
      return new Promise((resolve) => {
        syntaxHarness.replacements.push({
          replacement: request.replacement,
          resolve,
        });
      });
    }

    cancel(): void {
      this.instance.cancelCalls += 1;
    }

    dispose(): void {}
  },
}));

vi.mock("../regex/execution/EngineSupervisor", () => ({
  EngineSupervisor: class {
    result(request: RegexExecutionRequest) {
      return {
        accepted: true,
        engine: {
          id: "test-engine",
          displayName: "Test engine",
          version: "1",
          flavour: "ecmascript",
          offsetUnit: "utf16",
          runtime: "worker",
        },
        flags: {
          userFlags: request.flags.join(""),
          effectiveFlags: request.flags.join(""),
          internallyAddedIndicesFlag: false,
          internallyAddedGlobalFlag: false,
        },
        matches: engineHarness.emitWholeFirstCharacterMatch
          ? [
              {
                matchNumber: 1,
                value: request.subject.slice(0, 1),
                valueStatus: "complete" as const,
                range: { startUtf16: 0, endUtf16: 1 },
                nativeRange: { start: 0, end: 1, unit: "utf16" as const },
                captures: [],
              },
            ]
          : [],
        diagnostics: [],
        elapsedMs: 1,
        truncated: false,
      };
    }

    execute(request: RegexExecutionRequest) {
      engineHarness.requests.push(request);
      return Promise.resolve(this.result(request));
    }

    replace(
      request: RegexExecutionRequest & {
        readonly replacement: string;
      },
    ) {
      engineHarness.requests.push(request);
      return Promise.resolve({
        execution: this.result(request),
        output: request.replacement,
        outputBytes: request.replacement.length,
        outputTruncated: false,
        truncated: false,
      });
    }

    cancel(): void {}

    dispose(): void {}
  },
}));

import { Workbench } from "./Workbench";

function syntaxResult(
  pattern: string,
  captures: readonly CaptureDefinition[],
): RegexSyntaxResult {
  return {
    accepted: true,
    root: {
      id: `root-${pattern}`,
      kind: "pattern",
      range: { startUtf16: 0, endUtf16: pattern.length },
      raw: pattern,
      explanation: "Test syntax",
      children: [],
      properties: {
        zeroWidth: false,
        nullable: false,
        consumesInput: true,
      },
      support: {
        flavour: "ecmascript",
        status: "supported",
        notes: [],
      },
      provenance: {
        provider: "test",
        providerVersion: "1",
        source: "parsed",
      },
    },
    tokens: [],
    captures,
    diagnostics: [],
    provider: { id: "test", version: "1" },
    coverage: {
      status: "full-tested",
      summary: "Test coverage",
      unsupportedConstructs: [],
    },
  };
}

async function advanceParseDebounce(): Promise<void> {
  await act(async () => {
    await vi.advanceTimersByTimeAsync(120);
  });
}

async function resolvePattern(
  index: number,
  captures: readonly CaptureDefinition[],
): Promise<void> {
  await act(async () => {
    const pending = syntaxHarness.patterns[index];
    if (!pending) throw new Error(`Pattern request ${index} is unavailable`);
    pending.resolve(syntaxResult(pending.request.pattern, captures));
    await Promise.resolve();
  });
}

async function resolveLatestReplacement(): Promise<void> {
  await act(async () => {
    const pending = syntaxHarness.replacements.at(-1);
    if (!pending) throw new Error("Replacement request is unavailable");
    pending.resolve({
      accepted: true,
      tokens: [],
      diagnostics: [],
      totalDiagnostics: 0,
      diagnosticsTruncated: false,
    });
    await Promise.resolve();
  });
}

afterEach(() => {
  cleanup();
  vi.useRealTimers();
  syntaxHarness.instances.length = 0;
  syntaxHarness.patterns.length = 0;
  syntaxHarness.replacements.length = 0;
  engineHarness.requests.length = 0;
  engineHarness.emitWholeFirstCharacterMatch = false;
});

describe("Workbench syntax coordination", () => {
  it("exposes enumerated engine options and forwards the selected C++ grammar", async () => {
    vi.useFakeTimers();
    render(<Workbench />);

    fireEvent.change(screen.getByRole("combobox", { name: "Flavour" }), {
      target: { value: "cpp" },
    });
    const grammar = screen.getByRole("combobox", { name: "Grammar" });
    expect(grammar).toHaveValue("ECMAScript");

    fireEvent.change(grammar, { target: { value: "extended" } });
    await advanceParseDebounce();

    expect(syntaxHarness.patterns.at(-1)?.request).toMatchObject({
      flavour: "cpp",
      flags: ["g"],
      options: { grammar: "extended" },
    });
  });

  it("blocks Run until current syntax arrives and isolates replacement parsing", async () => {
    vi.useFakeTimers();
    render(<Workbench />);
    fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));

    await advanceParseDebounce();
    expect(syntaxHarness.patterns[0]?.request).toMatchObject({
      flavour: "ecmascript",
      flavourVersion: "2025",
      flags: ["g", "u"],
      options: {},
    });
    expect(
      screen.getByRole("combobox", { name: "Flavour" }),
    ).not.toBeDisabled();
    expect(screen.getByRole("combobox", { name: "Version" })).toBeDisabled();
    await resolvePattern(0, [
      {
        number: 1,
        name: "old",
        range: { startUtf16: 0, endUtf16: 9 },
        repeated: false,
      },
    ]);

    expect(syntaxHarness.instances.map((instance) => instance.label)).toEqual([
      "Pattern syntax parser",
      "Replacement syntax parser",
    ]);

    fireEvent.click(screen.getByRole("button", { name: "Replace" }));
    const patternInput = screen.getByRole("textbox", {
      name: "Regular expression pattern",
    });
    const replacementInput = screen.getByRole("textbox", {
      name: "Replacement template",
    });

    fireEvent.change(patternInput, { target: { value: "(?<new>b)" } });
    fireEvent.change(replacementInput, { target: { value: "$<new>" } });

    const runButton = screen.getByRole("button", { name: "Run" });
    expect(runButton).toBeDisabled();
    fireEvent.click(runButton);
    expect(engineHarness.requests).toHaveLength(0);

    await advanceParseDebounce();
    expect(syntaxHarness.patterns[1]?.request.pattern).toBe("(?<new>b)");
    await resolvePattern(1, [
      {
        number: 1,
        name: "new",
        range: { startUtf16: 0, endUtf16: 9 },
        repeated: false,
      },
    ]);

    expect(runButton).toBeDisabled();
    await resolveLatestReplacement();
    expect(runButton).toBeEnabled();
    const patternParser = syntaxHarness.instances[0];
    const replacementParser = syntaxHarness.instances[1];
    if (!patternParser || !replacementParser) {
      throw new Error("Expected both syntax parser instances");
    }
    const patternCancelCount = patternParser.cancelCalls;
    fireEvent.change(replacementInput, { target: { value: "$<new>!" } });
    fireEvent.change(replacementInput, { target: { value: "[$<new>]" } });

    expect(patternParser.cancelCalls).toBe(patternCancelCount);
    expect(replacementParser.cancelCalls).toBeGreaterThan(0);
    expect(
      syntaxHarness.replacements.map((request) => request.replacement),
    ).toContain("[$<new>]");

    expect(runButton).toBeDisabled();
    await resolveLatestReplacement();
    expect(runButton).toBeEnabled();
    fireEvent.click(runButton);
    await act(async () => {
      await Promise.resolve();
    });
    expect(engineHarness.requests).toHaveLength(1);
    expect(engineHarness.requests[0]).toMatchObject({
      flavour: "ecmascript",
      flavourVersion: "ECMAScript 2025 syntax / current browser runtime",
      options: {},
      pattern: "(?<new>b)",
      captureMetadata: [{ number: 1, name: "new" }],
    });
  });

  it("does not revive an old syntax snapshot when rapid edits return to the same text", async () => {
    vi.useFakeTimers();
    render(<Workbench />);
    fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));

    await advanceParseDebounce();
    await resolvePattern(0, []);
    const patternInput = screen.getByRole("textbox", {
      name: "Regular expression pattern",
    });
    const initialPattern = (patternInput as HTMLTextAreaElement).value;
    const runButton = screen.getByRole("button", { name: "Run" });
    expect(runButton).toBeEnabled();

    fireEvent.change(patternInput, { target: { value: "temporary" } });
    fireEvent.change(patternInput, { target: { value: initialPattern } });

    expect(runButton).toBeDisabled();
    fireEvent.click(screen.getByRole("treeitem"));
    expect(patternInput).toHaveAttribute("data-mark-count", "0");
    await advanceParseDebounce();
    await resolvePattern(1, []);
    expect(patternInput).toHaveAttribute("data-mark-count", "0");
    expect(runButton).toBeEnabled();
  });

  it("retains one exact result frame until the edited subject has a replacement frame", async () => {
    vi.useFakeTimers();
    engineHarness.emitWholeFirstCharacterMatch = true;
    render(<Workbench />);
    fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));

    await advanceParseDebounce();
    await resolvePattern(0, []);
    fireEvent.click(screen.getByRole("button", { name: "Run" }));
    await act(async () => {
      await Promise.resolve();
    });

    const subjectInput = screen.getByRole("textbox", { name: "Test text" });
    const originalSubject = (subjectInput as HTMLTextAreaElement).value;
    expect(subjectInput).toHaveAttribute("data-mark-count", "1");
    expect(document.querySelector(".extraction-row")).toHaveTextContent("2");

    fireEvent.change(subjectInput, { target: { value: "Z new subject" } });

    expect(screen.getByTestId("stale-result-frame")).toHaveTextContent(
      "bound to the last completed inputs",
    );
    expect(subjectInput).toHaveAttribute("data-mark-count", "0");
    expect(document.querySelector(".extraction-row")).toHaveTextContent("2");

    fireEvent.change(subjectInput, { target: { value: originalSubject } });
    expect(screen.getByTestId("stale-result-frame")).toBeInTheDocument();
    expect(subjectInput).toHaveAttribute("data-mark-count", "0");

    fireEvent.change(subjectInput, { target: { value: "Z new subject" } });
    (subjectInput as HTMLTextAreaElement).setSelectionRange(0, 1);
    fireEvent.select(subjectInput);
    expect(
      document.querySelector(".extraction-row.is-selected"),
    ).not.toBeInTheDocument();

    fireEvent.click(screen.getByRole("button", { name: "Run" }));
    await act(async () => {
      await Promise.resolve();
    });

    expect(screen.queryByTestId("stale-result-frame")).not.toBeInTheDocument();
    expect(subjectInput).toHaveAttribute("data-mark-count", "1");
    expect(document.querySelector(".extraction-row")).toHaveTextContent("Z");
  });

  it("captures a complete result and reruns only the selected saved test", async () => {
    vi.useFakeTimers();
    render(<Workbench />);
    fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));

    await advanceParseDebounce();
    await resolvePattern(0, []);
    fireEvent.click(screen.getByRole("button", { name: "Run" }));
    await act(async () => {
      await Promise.resolve();
    });

    fireEvent.click(screen.getByRole("button", { name: "Unit tests" }));
    const addCurrent = screen.getByRole("button", {
      name: "Add current result",
    });
    expect(addCurrent).toBeEnabled();
    fireEvent.click(addCurrent);

    expect(screen.getByText("Current match result")).toBeVisible();
    expect(screen.getByText("match count: 0", { exact: false })).toBeVisible();
    fireEvent.click(
      screen.getByRole("checkbox", { name: "Select Current match result" }),
    );
    fireEvent.click(screen.getByRole("button", { name: "Run selected" }));

    const testPatternIndex = syntaxHarness.patterns.length - 1;
    await resolvePattern(testPatternIndex, []);
    await act(async () => {
      await Promise.resolve();
    });

    expect(screen.getByText(/^Pass —/)).toBeVisible();
    expect(engineHarness.requests.at(-1)?.pattern).toBe(
      syntaxHarness.patterns[0]?.request.pattern,
    );
    expect(engineHarness.requests.at(-1)?.scanAll).toBe(false);
  });

  it("hard-stops an active suite at the aggregate wall deadline", async () => {
    vi.useFakeTimers();
    render(<Workbench />);
    fireEvent.click(screen.getByRole("checkbox", { name: "Live" }));

    await advanceParseDebounce();
    await resolvePattern(0, []);
    fireEvent.click(screen.getByRole("button", { name: "Run" }));
    await act(async () => {
      await Promise.resolve();
    });
    fireEvent.click(screen.getByRole("button", { name: "Unit tests" }));
    fireEvent.click(screen.getByRole("button", { name: "Add current result" }));
    fireEvent.click(screen.getByRole("button", { name: "Run all" }));
    expect(screen.getByRole("button", { name: "Cancel" })).toBeVisible();

    await act(async () => {
      await vi.advanceTimersByTimeAsync(60_000);
    });

    expect(screen.getByText(/Suite stopped at the 60 second/u)).toBeVisible();
    expect(screen.getByRole("button", { name: "Run all" })).toBeEnabled();
    expect(
      syntaxHarness.instances.find(
        (instance) => instance.label === "Syntax parser",
      )?.cancelCalls,
    ).toBeGreaterThan(0);
  });

  it("opens the bounded subject minimizer with the current editor snapshot", () => {
    vi.useFakeTimers();
    render(<Workbench />);

    fireEvent.click(screen.getByRole("button", { name: "Minimize" }));

    expect(
      screen.getByRole("heading", { name: "Minimize a reproducer" }),
    ).toBeVisible();
    expect(screen.getByLabelText("Subject to minimize")).toHaveValue(
      "2026-07-24 alice\nInvalid row\n2025-12-31 Bérénice",
    );
    expect(screen.getByLabelText("Minimization failure kind")).toHaveValue(
      "unit-test-failure",
    );
    fireEvent.click(
      screen.getByRole("button", { name: "Close subject minimizer" }),
    );
    expect(
      screen.queryByRole("heading", { name: "Minimize a reproducer" }),
    ).not.toBeInTheDocument();
  });

  it("opens and closes the grammar-backed formatter workspace", () => {
    vi.useFakeTimers();
    render(<Workbench />);

    fireEvent.click(screen.getByRole("button", { name: "Format" }));

    expect(
      screen.getByRole("heading", { name: "Format pattern" }),
    ).toBeVisible();
    expect(
      screen.getByText(/Wait for the ECMAScript syntax provider/),
    ).toBeVisible();
    fireEvent.click(
      screen.getByRole("button", { name: "Close pattern formatter" }),
    );
    expect(
      screen.queryByRole("heading", { name: "Format pattern" }),
    ).not.toBeInTheDocument();
  });
});
