import {
  act,
  cleanup,
  fireEvent,
  render,
  screen,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { RegexExecutionRequest } from "../regex/model/match";

const engineHarness = vi.hoisted(() => ({
  cancelCalls: 0,
  disposeCalls: 0,
  executeRequests: [] as RegexExecutionRequest[],
}));

vi.mock("../regex/execution/EngineSupervisor", () => ({
  EngineSupervisor: class {
    execute(request: RegexExecutionRequest) {
      engineHarness.executeRequests.push(request);
      return Promise.resolve({
        accepted: true,
        engine: {
          flavour: "ecmascript",
          adapterVersion: "test",
          engineName: "Test engine",
          engineVersion: "1",
          runtimeVersion: "1",
          offsetUnit: "utf16",
          capabilities: {
            compilation: true,
            matching: true,
            replacement: true,
            namedCaptures: true,
            captureHistory: false,
            actualTrace: false,
            benchmark: false,
          },
        },
        flags: {
          userFlags: "",
          effectiveFlags: "g",
          internallyAddedIndicesFlag: true,
          internallyAddedGlobalFlag: true,
        },
        matches: [
          {
            matchNumber: 1,
            value: "42",
            valueStatus: "complete",
            range: { startUtf16: 0, endUtf16: 2 },
            nativeRange: { start: 0, end: 2, unit: "utf16" },
            captures: [],
          },
        ],
        diagnostics: [],
        elapsedMs: 2,
        truncated: false,
      });
    }

    replace(): Promise<never> {
      return Promise.reject(new Error("Unexpected replacement call"));
    }

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

    dispose(): void {
      engineHarness.disposeCalls += 1;
    }
  },
}));

import { CorpusPanel } from "./CorpusPanel";

afterEach(() => {
  cleanup();
  engineHarness.cancelCalls = 0;
  engineHarness.disposeCalls = 0;
  engineHarness.executeRequests.length = 0;
});

function renderPanel(
  overrides: Partial<Parameters<typeof CorpusPanel>[0]> = {},
) {
  return render(
    <CorpusPanel
      active
      flavour="ecmascript"
      flavourVersion="2025"
      options={{}}
      pattern="\\d+"
      flags={[]}
      captureMetadata={[]}
      patternAccepted
      replacement="[$&]"
      replacementAccepted
      onReplacementChange={() => {}}
      timeoutMs={2_000}
      {...overrides}
    />,
  );
}

describe("CorpusPanel", () => {
  it("adds pasted text and produces an exhaustive per-document summary", async () => {
    renderPanel();
    await userEvent.type(
      screen.getByRole("textbox", { name: "Pasted corpus text" }),
      "42",
    );
    await userEvent.click(
      screen.getByRole("button", { name: "Add pasted text" }),
    );

    expect(
      screen.getByRole("list", { name: "Corpus documents" }),
    ).toHaveTextContent("pasted-text.txt");
    await userEvent.click(screen.getByRole("button", { name: "Scan corpus" }));

    expect(
      await screen.findByRole("table", { name: "Corpus document results" }),
    ).toHaveTextContent("complete");
    expect(engineHarness.executeRequests).toHaveLength(1);
    expect(engineHarness.executeRequests[0]).toMatchObject({
      subject: "42",
      scanAll: true,
    });
    expect(
      screen.getByRole("button", { name: "Download summary JSON" }),
    ).toBeEnabled();
  });

  it("blocks invalid runs and clears results when configuration changes", async () => {
    const rendered = renderPanel({ patternAccepted: false });
    fireEvent.click(screen.getByRole("button", { name: "Add pasted text" }));
    expect(screen.getByRole("button", { name: "Scan corpus" })).toBeDisabled();
    expect(screen.getByRole("alert")).toHaveTextContent(
      "syntax provider accepts",
    );

    rendered.rerender(
      <CorpusPanel
        active
        flavour="ecmascript"
        flavourVersion="2025"
        options={{}}
        pattern="changed"
        flags={[]}
        captureMetadata={[]}
        patternAccepted
        replacement="[$&]"
        replacementAccepted
        onReplacementChange={() => {}}
        timeoutMs={2_000}
      />,
    );
    expect(screen.getByRole("button", { name: "Scan corpus" })).toBeEnabled();
    expect(screen.getByRole("status")).toHaveTextContent(
      "previous corpus results were cleared",
    );
  });

  it("cancels an active batch when the workspace is left", async () => {
    let resolveExecution: ((value: never) => void) | undefined;
    const pending = new Promise<never>((resolve) => {
      resolveExecution = resolve;
    });
    const execute = vi
      .spyOn(
        (await import("../regex/execution/EngineSupervisor")).EngineSupervisor
          .prototype,
        "execute",
      )
      .mockReturnValue(pending);
    const rendered = renderPanel();
    fireEvent.click(screen.getByRole("button", { name: "Add pasted text" }));
    fireEvent.click(screen.getByRole("button", { name: "Scan corpus" }));
    expect(
      screen.getByRole("button", { name: "Cancel corpus run" }),
    ).toBeEnabled();

    rendered.rerender(
      <CorpusPanel
        active={false}
        flavour="ecmascript"
        flavourVersion="2025"
        options={{}}
        pattern="\\d+"
        flags={[]}
        captureMetadata={[]}
        patternAccepted
        replacement="[$&]"
        replacementAccepted
        onReplacementChange={() => {}}
        timeoutMs={2_000}
      />,
    );
    expect(engineHarness.cancelCalls).toBeGreaterThan(0);
    execute.mockRestore();
    await act(async () => {
      resolveExecution?.(undefined as never);
    });
  });

  it("does not publish stale batch results after configuration changes", async () => {
    let resolveExecution: ((value: never) => void) | undefined;
    const pending = new Promise<never>((resolve) => {
      resolveExecution = resolve;
    });
    const execute = vi
      .spyOn(
        (await import("../regex/execution/EngineSupervisor")).EngineSupervisor
          .prototype,
        "execute",
      )
      .mockReturnValue(pending);
    const rendered = renderPanel();
    fireEvent.click(screen.getByRole("button", { name: "Add pasted text" }));
    fireEvent.click(screen.getByRole("button", { name: "Scan corpus" }));

    rendered.rerender(
      <CorpusPanel
        active
        flavour="ecmascript"
        flavourVersion="2025"
        options={{}}
        pattern="changed"
        flags={[]}
        captureMetadata={[]}
        patternAccepted
        replacement="[$&]"
        replacementAccepted
        onReplacementChange={() => {}}
        timeoutMs={2_000}
      />,
    );
    await act(async () => {
      resolveExecution?.(undefined as never);
    });

    expect(
      screen.queryByRole("table", { name: "Corpus document results" }),
    ).not.toBeInTheDocument();
    expect(screen.getByRole("status")).toHaveTextContent(
      "previous corpus results were cleared",
    );
    execute.mockRestore();
  });
});
