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

import { DIProvider } from "@/app/di";
import {
  MockAgentGateway,
  MockLayoutGateway,
  MockSystemGateway,
  MockWorkStateGateway,
} from "@/adapters/mock";
import { leaves } from "@/features/layout/layout";
import type { Gateways } from "@/ports";
import {
  ProjectWorkStatePanel,
  WorkStatePanelErrorBoundary,
} from "./ProjectWorkStatePanel";

const PROJECT_ID = "project-work-state-test";

function renderPanel(
  workState: MockWorkStateGateway = new MockWorkStateGateway(),
  system: MockSystemGateway = new MockSystemGateway(),
  overrides: Partial<Gateways> = {},
) {
  const gateways = { workState, system, ...overrides } as unknown as Gateways;
  return {
    workState,
    system,
    ...render(
      <DIProvider gateways={gateways}>
        <ProjectWorkStatePanel projectId={PROJECT_ID} />
      </DIProvider>,
    ),
  };
}

describe("ProjectWorkStatePanel", () => {
  it("renders the empty state", async () => {
    renderPanel();

    expect(await screen.findByText("No agent work state.")).toBeTruthy();
  });

  it("renders an idle offline agent", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-1",
          name: "Planner",
          profileId: "codex",
          busy: { state: "idle" },
        },
      ],
    });

    renderPanel(workState);

    expect(await screen.findByText("Planner")).toBeTruthy();
    expect(screen.getByText("Offline")).toBeTruthy();
    expect(screen.getByText("Idle")).toBeTruthy();
    expect(screen.queryByLabelText("Planner tickets")).toBeNull();
  });

  it("renders a live busy agent with a short ticket", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-2",
          name: "Builder",
          profileId: "claude",
          live: {
            nodeId: "node-a",
            sessionId: "session-a",
            kind: "pty",
          },
          busy: {
            state: "busy",
            ticket: "4c65d981-da5e-412d-85ed-3b9779b670fa",
            sinceMs: 123,
          },
        },
      ],
    });

    renderPanel(workState);

    expect(await screen.findByText("Builder")).toBeTruthy();
    expect(screen.getByText("Live")).toBeTruthy();
    expect(screen.getByText("Busy")).toBeTruthy();
    expect(screen.getByLabelText("busy ticket 4c65d981")).toBeTruthy();
  });

  it("renders an in-progress human ticket", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-4",
          name: "Implementer",
          profileId: "codex",
          busy: {
            state: "busy",
            ticket: "ticket-in-progress-abcdef",
            sinceMs: 789,
          },
          tickets: [
            {
              ticketId: "ticket-in-progress-abcdef",
              conversationId: "conversation-a",
              position: 0,
              status: "inProgress",
              source: { kind: "human" },
              requesterLabel: "Anthony",
              taskPreview: "Wire the workstate queue UI",
              taskLen: 36,
            },
          ],
        },
      ],
    });

    renderPanel(workState);

    const list = await screen.findByLabelText("Implementer tickets");
    expect(within(list).getByText("#1 In progress")).toBeTruthy();
    expect(within(list).getByText("Human")).toBeTruthy();
    expect(within(list).getByText("Wire the workstate queue UI")).toBeTruthy();
    expect(within(list).getByLabelText("ticket ticket-i")).toBeTruthy();
    expect(within(list).getByLabelText("9 more characters")).toBeTruthy();
  });

  it("renders two tickets in FIFO order", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-5",
          name: "Reviewer",
          profileId: "claude",
          busy: { state: "idle" },
          tickets: [
            {
              ticketId: "ticket-second-abcdef",
              conversationId: "conversation-b",
              position: 1,
              status: "queued",
              source: { kind: "agent", agentId: "agent-main-123456" },
              requesterLabel: "Main",
              taskPreview: "Then review the result",
              taskLen: 22,
            },
            {
              ticketId: "ticket-first-abcdef",
              conversationId: "conversation-a",
              position: 0,
              status: "inProgress",
              source: { kind: "agent", agentId: "agent-qa-123456" },
              requesterLabel: "QA",
              taskPreview: "Check behavior",
              taskLen: 14,
            },
          ],
        },
      ],
    });

    renderPanel(workState);

    const list = await screen.findByLabelText("Reviewer tickets");
    expect(within(list).getByText("QA (agent-qa)")).toBeTruthy();
    expect(within(list).getByText("Main (agent-ma)")).toBeTruthy();
    expect(list.textContent?.indexOf("#1 In progress")).toBeLessThan(
      list.textContent?.indexOf("#2 Queued") ?? -1,
    );
  });

  it("normalizes the real backend background-task payload shape", async () => {
    // Mirrors AgentBackgroundTaskStateDto (crates/app-tauri/src/dto.rs): camelCase,
    // `state` (not `status`), optional exitCode/summary/stdoutTail/stderrTail omitted
    // when absent, createdAtMs/updatedAtMs present, NO ownerAgentId/projectId/finishedAtMs.
    // Headless rendezvous tasks may carry requester/target/conversation context.
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-bg",
          name: "Runner",
          profileId: "codex",
          busy: { state: "idle" },
          backgroundTasks: [
            {
              taskId: "task-waiting-1",
              kind: "command",
              state: "waiting",
              createdAtMs: 10,
              updatedAtMs: 11,
            },
            {
              taskId: "task-queued-1",
              kind: "headlessRendezvous",
              state: "queued",
              requesterAgentId: "agent-main",
              targetAgentId: "agent-bg",
              conversationId: "conversation-rdv",
              createdAtMs: 12,
              updatedAtMs: 13,
            },
            {
              taskId: "task-failed-1",
              kind: "command",
              state: "failed",
              exitCode: 1,
              summary: "process failed",
              stderrTail: "boom",
              createdAtMs: 14,
              updatedAtMs: 15,
            },
            {
              taskId: "task-expired-1",
              kind: "sessionResume",
              state: "expired",
              createdAtMs: 16,
              updatedAtMs: 17,
            },
          ],
        },
      ],
    });

    // Adapter-level assertion: locks the mapping against the real payload.
    const state = await workState.getProjectWorkState(PROJECT_ID);
    const tasks = state.agents[0]?.backgroundTasks ?? [];
    expect(tasks.map((t) => [t.taskId, t.status])).toEqual([
      ["task-waiting-1", "pending"],
      ["task-queued-1", "pending"],
      ["task-failed-1", "failed"],
      ["task-expired-1", "cancelled"],
    ]);
    // Optional fields omitted by the backend normalize to null (not undefined/NaN).
    expect(tasks[0]?.exitCode).toBeNull();
    expect(tasks[0]?.stdoutTail).toBeNull();
    expect(tasks[0]?.summary).toBeNull();
    expect(tasks[2]?.stderrTail).toBe("boom");
    // `summary` (ticket #5) is carried through when present.
    expect(tasks[2]?.summary).toBe("process failed");
    expect(tasks[1]?.requesterAgentId).toBe("agent-main");
    expect(tasks[1]?.targetAgentId).toBe("agent-bg");
    expect(tasks[1]?.conversationId).toBe("conversation-rdv");
    // updatedAtMs (backend-emitted) is carried through for chronological ordering.
    expect(tasks.map((t) => t.updatedAtMs)).toEqual([11, 13, 15, 17]);

    renderPanel(workState);

    const list = await screen.findByLabelText("Runner background tasks");
    const cancelButtons = within(list).getAllByRole("button", { name: "Cancel" });
    const retryButtons = within(list).getAllByRole("button", { name: "Retry" });
    // waiting + queued are cancellable; failed + expired are retryable.
    expect(cancelButtons.filter((b) => !(b as HTMLButtonElement).disabled)).toHaveLength(2);
    expect(retryButtons.filter((b) => !(b as HTMLButtonElement).disabled)).toHaveLength(2);

    // Rows are ordered most-recently-updated first (updatedAtMs desc), taskId as
    // the deterministic tiebreak — locks the fix for the dead finishedAtMs sort.
    const text = list.textContent ?? "";
    const order = ["task-exp", "task-fai", "task-que", "task-wai"].map((id) =>
      text.indexOf(id),
    );
    expect(order).toEqual([...order].sort((a, b) => a - b));
    expect(order.every((i) => i >= 0)).toBe(true);
  });

  it("shows a terminal task summary and hides it when absent or non-terminal (#5)", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-summary",
          name: "Reporter",
          profileId: "codex",
          busy: { state: "idle" },
          backgroundTasks: [
            {
              taskId: "run-with-summary",
              kind: "command",
              state: "running",
              summary: "should-not-appear-while-running",
              createdAtMs: 40,
              updatedAtMs: 40,
            },
            {
              taskId: "done-with-summary",
              kind: "command",
              state: "completed",
              summary: "Build succeeded in 12s",
              createdAtMs: 30,
              updatedAtMs: 30,
            },
            {
              taskId: "fail-with-summary",
              kind: "command",
              state: "failed",
              exitCode: 1,
              summary: "Compile error: missing semicolon",
              createdAtMs: 20,
              updatedAtMs: 20,
            },
            {
              taskId: "done-no-summary",
              kind: "command",
              state: "completed",
              createdAtMs: 10,
              updatedAtMs: 10,
            },
          ],
        },
      ],
    });

    renderPanel(workState);

    const list = await screen.findByLabelText("Reporter background tasks");
    // Terminal tasks with a summary render it discreetly.
    expect(
      within(list).getByLabelText("task done-wit summary").textContent,
    ).toBe("Build succeeded in 12s");
    expect(
      within(list).getByText("Compile error: missing semicolon"),
    ).toBeTruthy();
    // A completed task without a summary shows no summary line.
    expect(within(list).queryByLabelText("task done-no- summary")).toBeNull();
    // A still-running task never shows its (non-terminal) summary.
    expect(within(list).queryByLabelText("task run-with summary")).toBeNull();
    expect(
      within(list).queryByText("should-not-appear-while-running"),
    ).toBeNull();
  });

  it("attaches, repaints, streams, and detaches live background task output", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-live-task",
          name: "Runner",
          profileId: "codex",
          busy: { state: "idle" },
          backgroundTasks: [
            {
              taskId: "task-live-output",
              kind: "command",
              state: "running",
              createdAtMs: 10,
              updatedAtMs: 10,
            },
          ],
        },
      ],
    });
    workState._setBackgroundTaskAttachment("task-live-output", {
      scrollback: "already printed\n",
      live: true,
    });
    const attachSpy = vi.spyOn(workState, "attachBackgroundTask");

    const view = renderPanel(workState);
    const list = await screen.findByLabelText("Runner background tasks");

    fireEvent.click(within(list).getByRole("button", { name: "Live" }));

    expect(attachSpy).toHaveBeenCalledWith("task-live-output", expect.any(Function));
    expect(
      (await within(list).findByLabelText("task task-liv live output")).textContent,
    ).toContain("already printed");
    expect(workState._backgroundTaskSubscriberCount("task-live-output")).toBe(1);

    workState._emitBackgroundTaskOutput("task-live-output", "new line\n");
    await waitFor(() =>
      expect(
        within(list).getByLabelText("task task-liv live output").textContent,
      ).toContain("new line"),
    );

    fireEvent.click(within(list).getByRole("button", { name: "Detach" }));
    await waitFor(() =>
      expect(workState._backgroundTaskSubscriberCount("task-live-output")).toBe(0),
    );
    expect(within(list).queryByLabelText("task task-liv live output")).toBeNull();

    fireEvent.click(within(list).getByRole("button", { name: "Live" }));
    await within(list).findByLabelText("task task-liv live output");
    expect(workState._backgroundTaskSubscriberCount("task-live-output")).toBe(1);
    view.unmount();
    expect(workState._backgroundTaskSubscriberCount("task-live-output")).toBe(0);
  });

  it("renders a legacy agent without tickets", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-6",
          name: "Legacy",
          profileId: "gemini",
          busy: { state: "idle" },
        },
      ],
    });

    renderPanel(workState);

    expect(await screen.findByText("Legacy")).toBeTruthy();
    expect(screen.getByText("Offline")).toBeTruthy();
    expect(screen.getByText("Idle")).toBeTruthy();
    expect(screen.queryByLabelText("Legacy tickets")).toBeNull();
  });

  it("ignores a stray top-level backgroundTasks array (nested tasks intact) (#5)", async () => {
    // The backend nests tasks under their agent and emits no `ownerAgentId`; a
    // project-root `backgroundTasks` array is not part of the contract and must
    // not be merged in (dead path removed in ticket #5).
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-nested",
          name: "Nested",
          profileId: "codex",
          busy: { state: "idle" },
          backgroundTasks: [
            {
              taskId: "nested-1",
              kind: "command",
              state: "completed",
              createdAtMs: 1,
              updatedAtMs: 2,
            },
          ],
        },
      ],
      backgroundTasks: [
        {
          taskId: "orphan-top-level",
          kind: "command",
          state: "completed",
          ownerAgentId: "agent-nested",
          createdAtMs: 3,
          updatedAtMs: 4,
        },
      ],
    });

    const state = await workState.getProjectWorkState(PROJECT_ID);
    expect((state.agents[0]?.backgroundTasks ?? []).map((t) => t.taskId)).toEqual(
      ["nested-1"],
    );
  });

  it("normalizes missing top-level work state arrays", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {});

    await expect(workState.getProjectWorkState(PROJECT_ID)).resolves.toEqual({
      agents: [],
      conversations: [],
    });
  });

  it("renders an error fallback instead of blanking the app when Work throws", () => {
    function BrokenWorkPanel(): JSX.Element {
      throw new Error("render exploded");
    }

    render(
      <WorkStatePanelErrorBoundary>
        <BrokenWorkPanel />
      </WorkStatePanelErrorBoundary>,
    );

    expect(screen.getByRole("alert").textContent).toContain(
      "Work panel failed to render.",
    );
    expect(screen.getByText("render exploded")).toBeTruthy();
    expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
  });

  it("renders conversation objective and summary joined by conversation id", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-8",
          name: "Summarizer",
          profileId: "codex",
          busy: { state: "busy", ticket: "ticket-goal-abcdef", sinceMs: 123 },
          tickets: [
            {
              ticketId: "ticket-goal-abcdef",
              conversationId: "conversation-goal",
              position: 0,
              status: "inProgress",
              source: { kind: "human" },
              requesterLabel: "Anthony",
              taskPreview: "Continue implementation",
              taskLen: 23,
            },
            {
              ticketId: "ticket-summary-abcdef",
              conversationId: "conversation-summary",
              position: 1,
              status: "queued",
              source: { kind: "agent", agentId: "agent-main-123456" },
              requesterLabel: "Main",
              taskPreview: "Follow up",
              taskLen: 9,
            },
          ],
        },
      ],
      conversations: [
        {
          conversationId: "conversation-summary",
          status: "ready",
          objectivePreview: null,
          summaryPreview: "Previous turn established the adapter boundary.",
          summaryLen: 47,
          upTo: "turn-2",
          recentTurns: [],
        },
        {
          conversationId: "conversation-goal",
          status: "ready",
          objectivePreview: "Ship the Work panel summaries",
          summaryPreview: "This should be lower priority than the goal.",
          summaryLen: 48,
          upTo: "turn-1",
          recentTurns: [],
        },
      ],
    });

    renderPanel(workState);

    const list = await screen.findByLabelText("Summarizer tickets");
    expect(within(list).getAllByText("Summary")).toHaveLength(2);
    expect(
      within(list).getByText("Goal: Ship the Work panel summaries"),
    ).toBeTruthy();
    expect(
      within(list).getByText(
        "Previous turn established the adapter boundary.",
      ),
    ).toBeTruthy();
  });

  it("keeps partial and unavailable summaries compact without hiding tickets", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-9",
          name: "Contextualizer",
          profileId: "claude",
          busy: { state: "idle" },
          tickets: [
            {
              ticketId: "ticket-partial-abcdef",
              conversationId: "conversation-partial",
              position: 0,
              status: "inProgress",
              source: { kind: "agent", agentId: "agent-qa-123456" },
              requesterLabel: "QA",
              taskPreview: "Validate partial context",
              taskLen: 24,
            },
            {
              ticketId: "ticket-unavailable-abcdef",
              conversationId: "conversation-unavailable",
              position: 1,
              status: "queued",
              source: { kind: "human" },
              requesterLabel: "Anthony",
              taskPreview: "Validate unavailable context",
              taskLen: 28,
            },
          ],
        },
      ],
      conversations: [
        {
          conversationId: "conversation-partial",
          status: "partial",
          objectivePreview: null,
          summaryPreview: null,
          summaryLen: 0,
          upTo: null,
          recentTurns: [
            {
              role: "prompt",
              source: { kind: "agent", agentId: "agent-qa-123456" },
              atMs: 100,
              textPreview: "Only the latest prompt was available.",
              textLen: 37,
            },
          ],
        },
        {
          conversationId: "conversation-unavailable",
          status: "unavailable",
          objectivePreview: null,
          summaryPreview: "Summary service unavailable right now.",
          summaryLen: 38,
          upTo: null,
          recentTurns: [],
        },
      ],
    });

    renderPanel(workState);

    const list = await screen.findByLabelText("Contextualizer tickets");
    expect(within(list).getByText("#1 In progress")).toBeTruthy();
    expect(within(list).getByText("Validate partial context")).toBeTruthy();
    expect(within(list).getByText("Partial")).toBeTruthy();
    expect(
      within(list).getByText("Last: Only the latest prompt was available."),
    ).toBeTruthy();
    expect(within(list).getByText("#2 Queued")).toBeTruthy();
    expect(within(list).getByText("Validate unavailable context")).toBeTruthy();
    expect(within(list).getByText("Unavailable")).toBeTruthy();
    expect(
      within(list).getByText("Summary service unavailable right now."),
    ).toBeTruthy();
  });

  it("refreshes when a relevant domain event fires", async () => {
    const workState = new MockWorkStateGateway();
    const system = new MockSystemGateway();
    renderPanel(workState, system);

    await screen.findByText("No agent work state.");
    const spy = vi.spyOn(workState, "getProjectWorkState");
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-3",
          name: "Responder",
          profileId: "gemini",
          busy: { state: "busy", ticket: "ticket-123456789", sinceMs: 456 },
        },
      ],
    });

    system.emit({
      type: "agentBusyChanged",
      agentId: "agent-3",
      busy: true,
    });

    expect(await screen.findByText("Responder")).toBeTruthy();
    await waitFor(() => expect(spy).toHaveBeenCalled());
  });

  it("refreshes when delegationReady fires", async () => {
    const workState = new MockWorkStateGateway();
    const system = new MockSystemGateway();
    renderPanel(workState, system);

    await screen.findByText("No agent work state.");
    const spy = vi.spyOn(workState, "getProjectWorkState");
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-7",
          name: "Delegatee",
          profileId: "codex",
          busy: { state: "busy", ticket: "ticket-ready-123456", sinceMs: 123 },
          tickets: [
            {
              ticketId: "ticket-ready-123456",
              conversationId: "conversation-ready",
              position: 0,
              status: "inProgress",
              source: { kind: "agent", agentId: "agent-main-123456" },
              requesterLabel: "Main",
              taskPreview: "Handle this delegated task",
              taskLen: 26,
            },
          ],
        },
      ],
    });

    system.emit({
      type: "delegationReady",
      agentId: "agent-7",
      ticket: "ticket-ready-123456",
      text: "Handle this delegated task",
    });

    expect(await screen.findByText("Delegatee")).toBeTruthy();
    expect(screen.getByText("#1 In progress")).toBeTruthy();
    await waitFor(() => expect(spy).toHaveBeenCalled());
  });

  it("Open focuses a visible live cell and does not launch", async () => {
    const workState = new MockWorkStateGateway();
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const leafId = leaves(await layout.loadLayout(PROJECT_ID))[0].id;
    const host = document.createElement("div");
    host.dataset.nodeId = leafId;
    document.body.appendChild(host);
    const launchSpy = vi.spyOn(agent, "launchAgent");
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-open",
          name: "Visible",
          profileId: "codex",
          live: { nodeId: leafId, sessionId: "session-open", kind: "pty" },
          busy: { state: "idle" },
          tickets: [],
        },
      ],
      conversations: [],
    });

    renderPanel(workState, new MockSystemGateway(), { layout, agent });

    fireEvent.click(await screen.findByRole("button", { name: "Open" }));

    expect(host.style.outline).toContain("2px solid");
    expect(launchSpy).not.toHaveBeenCalled();
    host.remove();
  });

  it("Attach rebinds a hidden live agent to the deterministic empty cell", async () => {
    const workState = new MockWorkStateGateway();
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const created = await agent.createAgent(PROJECT_ID, {
      name: "Hidden",
      profileId: "codex",
    });
    await agent.launchAgent(
      PROJECT_ID,
      created.id,
      { cwd: "/x", rows: 24, cols: 80, nodeId: "hidden-node" },
      () => {},
    );
    const attachSpy = vi.spyOn(agent, "attachLiveAgent");
    const launchSpy = vi.spyOn(agent, "launchAgent");
    const target = leaves(await layout.loadLayout(PROJECT_ID))[0].id;
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: created.id,
          name: "Hidden",
          profileId: "codex",
          live: { nodeId: "hidden-node", sessionId: "mock-agent-session-1", kind: "pty" },
          busy: { state: "idle" },
          tickets: [],
        },
      ],
      conversations: [],
    });

    renderPanel(workState, new MockSystemGateway(), { layout, agent });

    const attach = await screen.findByRole("button", { name: "Attach" });
    await waitFor(() => expect((attach as HTMLButtonElement).disabled).toBe(false));
    fireEvent.click(attach);

    await waitFor(() =>
      expect(attachSpy).toHaveBeenCalledWith(PROJECT_ID, created.id, target),
    );
    await waitFor(async () => {
      const leaf = leaves(await layout.loadLayout(PROJECT_ID))[0];
      expect(leaf.agent).toBe(created.id);
      expect(leaf.session).toBe("mock-agent-session-1");
    });
    expect(launchSpy).not.toHaveBeenCalled();
  });

  it("disables Attach when no visible target cell exists", async () => {
    const workState = new MockWorkStateGateway();
    const layout = new MockLayoutGateway();
    const leafId = leaves(await layout.loadLayout(PROJECT_ID))[0].id;
    await layout.mutateLayout(PROJECT_ID, {
      type: "setCellAgent",
      target: leafId,
      agent: "other-agent",
    });
    await layout.mutateLayout(PROJECT_ID, {
      type: "setSession",
      target: leafId,
      session: "occupied-session",
    });
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-hidden",
          name: "Hidden",
          profileId: "codex",
          live: { nodeId: "hidden-node", sessionId: "session-hidden", kind: "pty" },
          busy: { state: "idle" },
          tickets: [],
        },
      ],
      conversations: [],
    });

    renderPanel(workState, new MockSystemGateway(), {
      layout,
      agent: new MockAgentGateway(),
    });

    const attach = await screen.findByRole("button", { name: "Attach" });
    await waitFor(() => expect((attach as HTMLButtonElement).disabled).toBe(true));
    expect(await screen.findByText("No empty visible cell.")).toBeTruthy();
  });

  it("Stop confirms with a busy warning, stops the live agent, and refreshes Work", async () => {
    const workState = new MockWorkStateGateway();
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const created = await agent.createAgent(PROJECT_ID, {
      name: "Runner",
      profileId: "codex",
    });
    const leafId = leaves(await layout.loadLayout(PROJECT_ID))[0].id;
    await agent.launchAgent(
      PROJECT_ID,
      created.id,
      { cwd: "/x", rows: 24, cols: 80, nodeId: leafId },
      () => {},
    );
    await layout.mutateLayout(PROJECT_ID, {
      type: "setCellAgent",
      target: leafId,
      agent: created.id,
    });
    await layout.mutateLayout(PROJECT_ID, {
      type: "setSession",
      target: leafId,
      session: "mock-agent-session-1",
    });
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: created.id,
          name: "Runner",
          profileId: "codex",
          live: { nodeId: leafId, sessionId: "mock-agent-session-1", kind: "pty" },
          busy: { state: "busy", ticket: "ticket-stop", sinceMs: 1 },
          tickets: [
            {
              ticketId: "ticket-stop",
              conversationId: "conv-stop",
              position: 0,
              status: "inProgress",
              source: { kind: "human" },
              requesterLabel: "Human",
              taskPreview: "Do work",
              taskLen: 7,
            },
          ],
        },
      ],
      conversations: [],
    });
    const stopSpy = vi.spyOn(agent, "stopLiveAgent");
    const refreshSpy = vi.spyOn(workState, "getProjectWorkState");
    const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);

    renderPanel(workState, new MockSystemGateway(), { layout, agent });

    fireEvent.click(await screen.findByRole("button", { name: "Stop" }));

    await waitFor(() =>
      expect(confirmSpy).toHaveBeenCalledWith(
        "Stop Runner? The current turn will be interrupted.",
      ),
    );
    await waitFor(() => expect(stopSpy).toHaveBeenCalledWith(PROJECT_ID, created.id));
    await waitFor(() => expect(refreshSpy).toHaveBeenCalled());
    await waitFor(async () => {
      const leaf = leaves(await layout.loadLayout(PROJECT_ID))[0];
      expect(leaf.session).toBeNull();
    });
    confirmSpy.mockRestore();
  });

  it("View conversation exposes only Lot C preview fields", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-view",
          name: "Viewer",
          profileId: "codex",
          busy: { state: "idle" },
          tickets: [
            {
              ticketId: "ticket-view",
              conversationId: "conversation-view",
              position: 0,
              status: "queued",
              source: { kind: "human" },
              requesterLabel: "Human",
              taskPreview: "Inspect preview",
              taskLen: 15,
            },
          ],
        },
      ],
      conversations: [
        {
          conversationId: "conversation-view",
          status: "ready",
          objectivePreview: "Keep it compact",
          summaryPreview: "Only the exposed summary preview.",
          summaryLen: 33,
          upTo: "internal-turn-id",
          recentTurns: [
            {
              role: "response",
              source: { kind: "agent", agentId: "agent-view" },
              atMs: 42,
              textPreview: "Recent exposed turn.",
              textLen: 20,
            },
          ],
        },
      ],
    });

    renderPanel(workState);

    const list = await screen.findByLabelText("Viewer tickets");
    fireEvent.click(within(list).getByRole("button", { name: "View" }));

    expect(within(list).getByText("Goal:")).toBeTruthy();
    expect(within(list).getByText("Keep it compact")).toBeTruthy();
    expect(within(list).getByText("Summary:")).toBeTruthy();
    expect(
      within(list).getByText("Only the exposed summary preview."),
    ).toBeTruthy();
    expect(within(list).getByText("response:")).toBeTruthy();
    expect(within(list).getByText("Recent exposed turn.")).toBeTruthy();
    expect(list.textContent).not.toContain("internal-turn-id");
  });

  it("Copy summary copies only the exposed preview text", async () => {
    const writeText = vi.fn().mockResolvedValue(undefined);
    vi.stubGlobal("navigator", { clipboard: { writeText } });
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState(PROJECT_ID, {
      agents: [
        {
          agentId: "agent-copy",
          name: "Copier",
          profileId: "codex",
          busy: { state: "idle" },
          tickets: [
            {
              ticketId: "ticket-copy",
              conversationId: "conversation-copy",
              position: 0,
              status: "queued",
              source: { kind: "human" },
              requesterLabel: "Human",
              taskPreview: "Copy preview",
              taskLen: 12,
            },
          ],
        },
      ],
      conversations: [
        {
          conversationId: "conversation-copy",
          status: "ready",
          objectivePreview: null,
          summaryPreview: "Copy this summary preview only.",
          summaryLen: 31,
          upTo: null,
          recentTurns: [
            {
              role: "prompt",
              source: { kind: "human" },
              atMs: 1,
              textPreview: "Do not copy this turn.",
              textLen: 22,
            },
          ],
        },
      ],
    });

    renderPanel(workState);

    const list = await screen.findByLabelText("Copier tickets");
    fireEvent.click(within(list).getByRole("button", { name: "Copy" }));

    await waitFor(() =>
      expect(writeText).toHaveBeenCalledWith("Copy this summary preview only."),
    );
    expect(await within(list).findByRole("button", { name: "Copied" })).toBeTruthy();
    vi.unstubAllGlobals();
  });
});
