import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { CaptureRow } from "../regex/extraction/capture-rows";
import { CaptureTable } from "./CaptureTable";

const rows: readonly CaptureRow[] = [
  {
    id: "row-2-1",
    matchNumber: 2,
    groupNumber: 1,
    groupName: "letter",
    status: "participated",
    value: "a",
    start: 2,
    end: 3,
    length: 1,
    nativeStart: 2,
    nativeEnd: 3,
    nativeUnit: "utf16",
    line: 1,
    column: 3,
  },
  {
    id: "row-1-1",
    matchNumber: 1,
    groupNumber: 1,
    groupName: "emoji",
    status: "participated",
    value: "😀",
    start: 0,
    end: 2,
    length: 2,
    nativeStart: 0,
    nativeEnd: 2,
    nativeUnit: "utf16",
    line: 1,
    column: 1,
  },
];

afterEach(() => {
  Reflect.deleteProperty(document, "execCommand");
});

describe("CaptureTable", () => {
  it("sorts, exposes Unicode offsets and copies values and rows separately", async () => {
    const writeText = vi.fn().mockResolvedValue(undefined);
    const user = userEvent.setup();
    Object.defineProperty(navigator, "clipboard", {
      configurable: true,
      value: { writeText },
    });
    render(
      <CaptureTable rows={rows} subject="😀a" onSelect={() => undefined} />,
    );

    const body = screen
      .getByRole("grid", {
        name: "Captured matches",
      })
      .querySelector("tbody");
    if (!body) throw new Error("Capture table body is missing");
    expect(within(body).getAllByRole("row")[0]).toHaveTextContent("1emoji");

    await user.click(screen.getByRole("button", { name: "Ascending" }));
    const firstDescendingRow = within(body).getAllByRole("row").at(0);
    if (!firstDescendingRow) throw new Error("Capture row is missing");
    expect(
      within(firstDescendingRow).getAllByRole("cell")[0],
    ).toHaveTextContent("2");
    expect(
      within(firstDescendingRow).getAllByRole("cell")[2],
    ).toHaveTextContent("letter");

    await user.click(screen.getByLabelText("Unicode advanced view"));
    expect(
      screen.getByRole("columnheader", { name: "Start (code point)" }),
    ).toBeInTheDocument();
    const emojiRow = within(body).getByRole("row", {
      name: /Match 1, group 1, emoji/u,
    });
    expect(emojiRow).toHaveTextContent("U+1F600");

    await user.click(
      screen.getByRole("button", {
        name: "Copy value for match 1, group 1",
      }),
    );
    expect(writeText).toHaveBeenLastCalledWith("😀");
    expect(screen.getByRole("status")).toHaveTextContent(
      /Copied the complete returned value/u,
    );
    await user.click(
      screen.getByRole("button", {
        name: "Copy row for match 1, group 1",
      }),
    );
    expect(writeText).toHaveBeenLastCalledWith(JSON.stringify(rows[1]));
  });

  it("moves to a synchronized row page and reports a filtered selection", async () => {
    const user = userEvent.setup();
    const pagedRows = Array.from({ length: 201 }, (_, index): CaptureRow => ({
      id: `row-${index + 1}-1`,
      matchNumber: index + 1,
      groupNumber: 1,
      status: "participated",
      value: `value-${index + 1}`,
      start: index,
      end: index + 1,
      length: 1,
      nativeStart: index,
      nativeEnd: index + 1,
      nativeUnit: "utf16",
      line: 1,
      column: index + 1,
    }));
    render(
      <CaptureTable
        rows={pagedRows}
        subject={"a".repeat(201)}
        selectedId="row-201-1"
        onSelect={() => undefined}
      />,
    );

    expect(screen.getByText(/Page 2 of 2/u)).toBeInTheDocument();
    expect(
      screen.getByRole("row", {
        name: /Match 201, group 1, participated/u,
      }),
    ).toHaveAttribute("aria-selected", "true");

    await user.type(screen.getByLabelText("Filter captures"), "missing");
    expect(
      screen.getByText(/synchronized capture row is hidden/u),
    ).toBeInTheDocument();
  });

  it("disables capture export for an engine-limited result prefix", () => {
    render(
      <CaptureTable
        rows={rows}
        subject="😀a"
        sourceResultTruncated
        onSelect={() => undefined}
      />,
    );

    expect(
      screen.getByRole("button", { name: "Export all JSON" }),
    ).toBeDisabled();
    expect(
      screen.getByText(/Capture JSON export is disabled/u),
    ).toBeInTheDocument();
  });

  it("bounds only the visible capture value while copying the complete value", async () => {
    const writeText = vi.fn().mockResolvedValue(undefined);
    const user = userEvent.setup();
    Object.defineProperty(navigator, "clipboard", {
      configurable: true,
      value: { writeText },
    });
    const value = `${"\n".repeat(600)}tail`;
    const row = {
      ...rows[0]!,
      value,
      end: value.length,
      length: value.length,
    };
    render(
      <CaptureTable rows={[row]} subject={value} onSelect={() => undefined} />,
    );

    expect(screen.getByRole("cell", { name: /UI preview/u })).toHaveTextContent(
      `${value.length.toLocaleString()} UTF-16 units total; UI preview`,
    );
    expect(
      screen.getByRole("cell", { name: /UI preview/u }).textContent,
    ).not.toContain("tail");
    await user.click(
      screen.getByRole("button", {
        name: "Copy value for match 2, group 1",
      }),
    );
    expect(writeText).toHaveBeenLastCalledWith(value);
  });

  it("labels a clipped engine value with the exact range length", () => {
    const value = "x".repeat(600);
    const row: CaptureRow = {
      ...rows[0]!,
      status: "truncated",
      value,
      start: 0,
      end: 70_000,
      length: 70_000,
    };
    render(
      <CaptureTable rows={[row]} subject={value} onSelect={() => undefined} />,
    );

    expect(
      screen.getByRole("cell", { name: /engine value prefix/u }),
    ).toHaveTextContent(
      /engine value prefix: 600 of 70,000 UTF-16 units; UI shows the first 512/u,
    );
  });

  it("reports unavailable clipboard access instead of rejecting silently", async () => {
    const user = userEvent.setup();
    Object.defineProperty(navigator, "clipboard", {
      configurable: true,
      value: undefined,
    });
    Object.defineProperty(document, "execCommand", {
      configurable: true,
      value: vi.fn().mockReturnValue(false),
    });
    render(
      <CaptureTable rows={rows} subject="😀a" onSelect={() => undefined} />,
    );

    await user.click(
      screen.getByRole("button", {
        name: "Copy value for match 1, group 1",
      }),
    );

    expect(await screen.findByRole("alert")).toHaveTextContent(
      /Clipboard access is unavailable/u,
    );
  });
});
