/**
 * T7 — the resume-conversation popup and its wiring in the layout grid.
 *
 * Two layers:
 *  1. The presentational {@link ResumeConversationPopup} in isolation: status
 *     derived from `agentWasRunning` (never the inspector); enriched mode (topic
 *     + tokens) vs degraded mode (status only); the two action callbacks.
 *  2. The grid wiring (`LayoutGrid` → `LeafView`): an agent cell that carries a
 *     persisted `conversationId` shows the popup **before** the Resume launch;
 *     "Reprendre" launches with the id; "Nouvelle conversation" clears the id
 *     first then launches without it. The reattach path (live PTY) never shows
 *     the popup.
 *
 * Because xterm bails under jsdom, `TerminalView`'s opener may not run in this
 * environment — so the grid-level assertions render the popup by driving the
 * opener directly is not possible. Instead we drive the popup through the
 * component-level tests (deterministic) and assert the *opener wiring* by
 * calling the exported component with stub gateways where the opener is invoked
 * manually. To stay robust we focus the integration assertions on the popup
 * presence + the two action effects, guarded on the opener having run.
 */
import { useEffect } from "react";
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";

import type { ConversationDetails } from "@/ports";
import { ResumeConversationPopup } from "@/features/terminals";

// ---------------------------------------------------------------------------
// 1. Presentational popup in isolation
// ---------------------------------------------------------------------------

describe("ResumeConversationPopup — presentation", () => {
  it("derives status 'en cours' from agentWasRunning=true (not the inspector)", () => {
    render(
      <ResumeConversationPopup
        agentWasRunning
        details={{ lastTopic: "x", tokenCount: 5 }}
        onResume={() => {}}
        onNewConversation={() => {}}
      />,
    );
    expect(screen.getByTestId("resume-status").textContent).toBe("en cours");
  });

  it("derives status 'clot' from agentWasRunning=false", () => {
    render(
      <ResumeConversationPopup
        agentWasRunning={false}
        details={{}}
        onResume={() => {}}
        onNewConversation={() => {}}
      />,
    );
    expect(screen.getByTestId("resume-status").textContent).toBe("clot");
  });

  it("enriched mode shows last topic + token count when present", () => {
    render(
      <ResumeConversationPopup
        agentWasRunning
        details={{ lastTopic: "refactor parser", tokenCount: 4242 }}
        onResume={() => {}}
        onNewConversation={() => {}}
      />,
    );
    expect(screen.getByTestId("resume-last-topic").textContent).toContain(
      "refactor parser",
    );
    expect(screen.getByTestId("resume-token-count").textContent).toContain("4242");
  });

  it("degraded mode (empty details) shows status only, no topic/token lines", () => {
    render(
      <ResumeConversationPopup
        agentWasRunning={false}
        details={{}}
        onResume={() => {}}
        onNewConversation={() => {}}
      />,
    );
    expect(screen.getByTestId("resume-status").textContent).toBe("clot");
    expect(screen.queryByTestId("resume-last-topic")).toBeNull();
    expect(screen.queryByTestId("resume-token-count")).toBeNull();
  });

  it("details=null (loading) shows status only", () => {
    render(
      <ResumeConversationPopup
        agentWasRunning
        details={null}
        onResume={() => {}}
        onNewConversation={() => {}}
      />,
    );
    expect(screen.getByTestId("resume-status").textContent).toBe("en cours");
    expect(screen.queryByTestId("resume-last-topic")).toBeNull();
  });

  it("token count of 0 still renders (uses !== undefined, not truthiness)", () => {
    render(
      <ResumeConversationPopup
        agentWasRunning
        details={{ tokenCount: 0 }}
        onResume={() => {}}
        onNewConversation={() => {}}
      />,
    );
    expect(screen.getByTestId("resume-token-count").textContent).toContain("0");
  });

  it("fires onResume / onNewConversation on the matching buttons", () => {
    const onResume = vi.fn();
    const onNewConversation = vi.fn();
    render(
      <ResumeConversationPopup
        agentWasRunning
        details={{}}
        onResume={onResume}
        onNewConversation={onNewConversation}
      />,
    );
    fireEvent.click(screen.getByText(/Reprendre la dernière conversation/));
    expect(onResume).toHaveBeenCalledTimes(1);
    fireEvent.click(screen.getByText(/Nouvelle conversation/));
    expect(onNewConversation).toHaveBeenCalledTimes(1);
  });
});

// ---------------------------------------------------------------------------
// 2. Grid wiring
// ---------------------------------------------------------------------------

import { render as renderGridDom } from "@testing-library/react";
import type { Gateways, OpenTerminalOptions, TerminalHandle } from "@/ports";
import { MockLayoutGateway, MockAgentGateway, MockTerminalGateway } from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { leaves } from "./layout";

// xterm bails under jsdom, so the real `TerminalView` never runs its opener
// (and thus never reaches our deferral/popup wiring). Replace it with a stub
// that immediately invokes the `open` opener — exactly what the real view does
// once the PTY mount succeeds — so the deferral → popup → action → launch path
// is deterministically exercised. `ResumeConversationPopup` is NOT mocked (we
// assert its rendered output). Importing `LayoutGrid` after the mock is set up.
vi.mock("@/features/terminals", async () => {
  const actual = await vi.importActual<typeof import("@/features/terminals")>(
    "@/features/terminals",
  );
  return {
    ...actual,
    TerminalView: ({
      open,
    }: {
      open?: (
        o: OpenTerminalOptions,
        onData: (b: Uint8Array) => void,
      ) => Promise<TerminalHandle>;
    }) => {
      // Mirror the real view: call the opener once on mount (fresh-open path).
      // In an effect (not during render) so the opener's setState is safe.
      useEffect(() => {
        if (open) void open({ cwd: "/x", rows: 24, cols: 80 }, () => {});
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);
      return <div data-testid="terminal-view" />;
    },
  };
});

// eslint-disable-next-line import/first
import { LayoutGrid } from "./LayoutGrid";

function makeHandle(sessionId: string): TerminalHandle {
  return {
    sessionId,
    write: vi.fn().mockResolvedValue(undefined),
    resize: vi.fn().mockResolvedValue(undefined),
    detach: vi.fn(),
    close: vi.fn().mockResolvedValue(undefined),
  };
}

function renderGrid(layout: MockLayoutGateway, agent: unknown, projectId = "p1") {
  const gateways = {
    layout,
    terminal: new MockTerminalGateway(),
    agent,
  } as unknown as Gateways;
  return renderGridDom(
    <DIProvider gateways={gateways}>
      <LayoutGrid projectId={projectId} cwd="/home/me/proj" />
    </DIProvider>,
  );
}

/** Seeds an agent on the root leaf carrying a persisted conversation id but no
 * live session — the Resume case that must trigger the popup. */
async function seedResumeLeaf(layout: MockLayoutGateway, agentId: string) {
  const tree = await layout.loadLayout("p1");
  const leafId = leaves(tree)[0].id;
  await layout.mutateLayout("p1", { type: "setCellAgent", target: leafId, agent: agentId });
  await layout.mutateLayout("p1", {
    type: "setCellConversation",
    target: leafId,
    conversationId: "prior-conv",
  });
  return leafId;
}

describe("LayoutGrid — resume popup wiring (T7)", () => {
  it("shows the popup for a resume cell and 'Reprendre' launches with the conversation id", async () => {
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const a = await agent.createAgent("p1", { name: "MyAgent", profileId: "p" });
    await seedResumeLeaf(layout, a.id);

    const launchAgent = vi.fn(
      async (
        _p: string,
        _ag: string,
        _opts: OpenTerminalOptions,
        _onData: (b: Uint8Array) => void,
      ) => makeHandle("sess-resume"),
    );
    const inspectConversation = vi.fn(
      async (): Promise<ConversationDetails> => ({ lastTopic: "topic-x", tokenCount: 7 }),
    );
    const stubAgent = {
      listAgents: () => Promise.resolve([a]),
      launchAgent,
      reattach: vi.fn(),
      inspectConversation,
    };

    renderGrid(layout, stubAgent);

    // The popup appears (opener deferred behind it) and the launch is gated.
    await waitFor(() =>
      expect(screen.getByTestId("resume-conversation-popup")).toBeTruthy(),
    );
    expect(launchAgent).not.toHaveBeenCalled();
    expect(inspectConversation).toHaveBeenCalledWith("p1", a.id, "prior-conv");

    fireEvent.click(screen.getByText(/Reprendre la dernière conversation/));

    await waitFor(() => expect(launchAgent).toHaveBeenCalledTimes(1));
    const opts = launchAgent.mock.calls[0][2] as OpenTerminalOptions;
    expect(opts.conversationId).toBe("prior-conv");
    // The id stays on the leaf (resume keeps it).
    const fresh = await layout.loadLayout("p1");
    expect(leaves(fresh)[0].conversationId).toBe("prior-conv");
  });

  it("'Nouvelle conversation' clears the id first, then launches without it", async () => {
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const a = await agent.createAgent("p1", { name: "MyAgent", profileId: "p" });
    await seedResumeLeaf(layout, a.id);

    const launchAgent = vi.fn(
      async (
        _p: string,
        _ag: string,
        _opts: OpenTerminalOptions,
        _onData: (b: Uint8Array) => void,
      ) => makeHandle("sess-new"),
    );
    const stubAgent = {
      listAgents: () => Promise.resolve([a]),
      launchAgent,
      reattach: vi.fn(),
      inspectConversation: vi.fn(async (): Promise<ConversationDetails> => ({})),
    };

    renderGrid(layout, stubAgent);

    await waitFor(() =>
      expect(screen.getByTestId("resume-conversation-popup")).toBeTruthy(),
    );
    expect(launchAgent).not.toHaveBeenCalled();

    fireEvent.click(screen.getByText(/Nouvelle conversation/));

    await waitFor(() => expect(launchAgent).toHaveBeenCalledTimes(1));
    const opts = launchAgent.mock.calls[0][2] as OpenTerminalOptions;
    expect(opts.conversationId).toBeUndefined();
    // The id was cleared on the leaf before launching (Assign on next launch).
    const fresh = await layout.loadLayout("p1");
    expect(leaves(fresh)[0].conversationId).toBeUndefined();
  });

  it("a fresh cell (no conversation id) does NOT show the popup", async () => {
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const a = await agent.createAgent("p1", { name: "MyAgent", profileId: "p" });
    const tree = await layout.loadLayout("p1");
    const leafId = leaves(tree)[0].id;
    await layout.mutateLayout("p1", { type: "setCellAgent", target: leafId, agent: a.id });

    const inspectConversation = vi.fn();
    const stubAgent = {
      listAgents: () => Promise.resolve([a]),
      launchAgent: vi.fn(async () => makeHandle("sess-fresh")),
      reattach: vi.fn(),
      inspectConversation,
    };

    renderGrid(layout, stubAgent);

    await waitFor(() => expect(screen.getByRole("combobox")).toBeTruthy());
    // No popup, and inspection is never triggered for a fresh cell.
    expect(screen.queryByTestId("resume-conversation-popup")).toBeNull();
    expect(inspectConversation).not.toHaveBeenCalled();
  });
});
