import { describe, it, expect, beforeEach, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";

import { DIProvider } from "@/app/di";
import {
  MockAgentGateway,
  MockSystemGateway,
  MockTicketGateway,
  MockUiPreferencesGateway,
} from "@/adapters/mock";
import type { Gateways } from "@/ports";
import { TicketsPanel } from "./TicketsPanel";
import { TicketPicker } from "./TicketPicker";
import { listFiltersKey, pickerFiltersKey } from "./ticketFilterPersistence";

const PROJECT_ID = "project-persist-test";

describe("ticket #29 — persisted list filters (useTickets)", () => {
  let system: MockSystemGateway;
  let ticket: MockTicketGateway;
  let agent: MockAgentGateway;
  let prefs: MockUiPreferencesGateway;

  beforeEach(() => {
    system = new MockSystemGateway();
    ticket = new MockTicketGateway(system);
    agent = new MockAgentGateway();
    prefs = new MockUiPreferencesGateway();
  });

  function renderPanel() {
    const gateways = { system, ticket, agent, uiPreferences: prefs } as unknown as Gateways;
    return render(
      <DIProvider gateways={gateways}>
        <TicketsPanel projectId={PROJECT_ID} onOpen={vi.fn()} />
      </DIProvider>,
    );
  }

  it("restores persisted list filters at mount", async () => {
    prefs.write(listFiltersKey(PROJECT_ID), {
      text: "restored-text",
      statuses: ["open"],
    });
    renderPanel();
    const search = (await screen.findByLabelText(
      "search tickets",
    )) as HTMLInputElement;
    expect(search.value).toBe("restored-text");
  });

  it("persists a filter change (scoped to the list key)", async () => {
    renderPanel();
    const search = (await screen.findByLabelText(
      "search tickets",
    )) as HTMLInputElement;
    fireEvent.change(search, { target: { value: "boom" } });
    await waitFor(() => {
      expect(prefs.read(listFiltersKey(PROJECT_ID))).toMatchObject({
        text: "boom",
      });
    });
    // The picker key stays untouched (surfaces do not pollute each other).
    expect(prefs.read(pickerFiltersKey(PROJECT_ID))).toBeNull();
  });

  it("drops an obsolete persisted assignee once the roster is known", async () => {
    prefs.write(listFiltersKey(PROJECT_ID), {
      text: "keep-me",
      assignedAgentId: "ghost-agent",
    });
    renderPanel();
    // The roster (empty here) has no such agent → the assignee is reconciled out
    // and re-persisted without it, while the rest of the filters survive.
    await waitFor(() => {
      const stored = prefs.read(listFiltersKey(PROJECT_ID)) as
        | Record<string, unknown>
        | null;
      expect(stored).not.toBeNull();
      expect(stored).toMatchObject({ text: "keep-me" });
      expect("assignedAgentId" in (stored ?? {})).toBe(false);
    });
  });
});

describe("ticket #29 — persisted picker filters (TicketPicker)", () => {
  let system: MockSystemGateway;
  let ticket: MockTicketGateway;
  let prefs: MockUiPreferencesGateway;

  beforeEach(() => {
    system = new MockSystemGateway();
    ticket = new MockTicketGateway(system);
    prefs = new MockUiPreferencesGateway();
  });

  function renderPicker(
    props: Partial<React.ComponentProps<typeof TicketPicker>> = {},
  ) {
    const gateways = { system, ticket, uiPreferences: prefs } as unknown as Gateways;
    return render(
      <DIProvider gateways={gateways}>
        <TicketPicker
          open
          projectId={PROJECT_ID}
          onSelect={vi.fn()}
          onClose={vi.fn()}
          {...props}
        />
      </DIProvider>,
    );
  }

  it("restores persisted picker filters on reopen", async () => {
    prefs.write(pickerFiltersKey(PROJECT_ID), { text: "picker-pref" });
    renderPicker();
    const search = (await screen.findByLabelText(
      "search tickets",
    )) as HTMLInputElement;
    expect(search.value).toBe("picker-pref");
  });

  it("lets initialQuery win over the persisted preference for imposed fields", async () => {
    await ticket.create(PROJECT_ID, { title: "Open item", status: "open" });
    const closed = await ticket.create(PROJECT_ID, { title: "Closed item" });
    await ticket.update(PROJECT_ID, closed.ref, {
      status: "closed",
      expectedVersion: closed.version,
    });
    // The stored preference asks for "closed", but the caller imposes "open".
    prefs.write(pickerFiltersKey(PROJECT_ID), { statuses: ["closed"] });
    renderPicker({ initialQuery: { statuses: ["open"] } });
    // initialQuery wins → the open item shows, the closed one is filtered out.
    expect(await screen.findByText("Open item")).toBeTruthy();
    expect(screen.queryByText("Closed item")).toBeNull();
  });

  it("never persists the opaque cursor, even after paginating", async () => {
    for (let i = 0; i < 3; i += 1) {
      await ticket.create(PROJECT_ID, { title: `Alpha ${i}` });
    }
    // Seed text so something is persisted; small limit forces a nextCursor.
    renderPicker({ initialQuery: { text: "Alpha", limit: 2 } });
    // Wait for the first page and its "Load more" (nextCursor present).
    const loadMore = await screen.findByRole("button", { name: /load more/i });
    fireEvent.click(loadMore);
    await screen.findByText("Alpha 0");

    await waitFor(() => {
      const stored = prefs.read(pickerFiltersKey(PROJECT_ID)) as
        | Record<string, unknown>
        | null;
      expect(stored).toMatchObject({ text: "Alpha" });
    });
    const stored = prefs.read(pickerFiltersKey(PROJECT_ID)) as Record<
      string,
      unknown
    >;
    expect("cursor" in stored).toBe(false);
    expect("limit" in stored).toBe(false);
  });
});
