/**
 * B2 — reopen resume flow (ARCHITECTURE §15.2) wired to the stateful
 * `MockAgentGateway` / `MockLayoutGateway` via the real `DIProvider`.
 *
 * Covers both layers visible from the frontend:
 * - **Adapter** (`TauriAgentGateway.listResumableAgents`): invokes
 *   `list_resumable_agents` with a top-level `{ projectId }` payload (NOT
 *   `{ request: … }`) and unwraps `{ resumable }` → `ResumableAgent[]`.
 * - **Panel** (`ResumeProjectPanel`): renders nothing on an empty inventory;
 *   lists the resumable agents (with their status) when non-empty.
 * - **Actions** (`useResumeProject`): Reprendre (with / without resume support),
 *   Nouvelle conversation, Ignorer, Tout reprendre / Tout ignorer — each
 *   asserting the exact `launchAgent` / `setCellConversation` calls.
 * - French labels on the main title + actions.
 */

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

import {
  MockAgentGateway,
  MockLayoutGateway,
} from "@/adapters/mock";
import type { Gateways } from "@/ports";
import type { ResumableAgent } from "@/domain";
import { DIProvider } from "@/app/di";
import { ResumeProjectPanel } from "./ResumeProjectPanel";

// ---------------------------------------------------------------------------
// Adapter: TauriAgentGateway.listResumableAgents payload + unwrap
// ---------------------------------------------------------------------------

const invokeMock = vi.fn();
vi.mock("@tauri-apps/api/core", () => ({
  invoke: (...args: unknown[]) => invokeMock(...args),
  Channel: class {},
}));

describe("TauriAgentGateway.listResumableAgents (adapter)", () => {
  beforeEach(() => invokeMock.mockReset());

  it("invokes list_resumable_agents with top-level projectId and unwraps { resumable }", async () => {
    const { TauriAgentGateway } = await import("@/adapters/agent");
    const wire: ResumableAgent[] = [
      {
        agentId: "a1",
        name: "Architect",
        nodeId: "n1",
        conversationId: "c1",
        wasRunning: true,
        resumeSupported: true,
      },
    ];
    invokeMock.mockResolvedValueOnce({ resumable: wire });

    const gw = new TauriAgentGateway();
    const out = await gw.listResumableAgents("proj-42");

    // Command name + EXACT payload: top-level projectId, not wrapped in request.
    expect(invokeMock).toHaveBeenCalledTimes(1);
    expect(invokeMock).toHaveBeenCalledWith("list_resumable_agents", {
      projectId: "proj-42",
    });
    const [, payload] = invokeMock.mock.calls[0];
    expect(payload).not.toHaveProperty("request");
    // Unwrapped to the bare array.
    expect(out).toEqual(wire);
  });
});

// ---------------------------------------------------------------------------
// Panel + actions: behind the real DIProvider with stateful mocks
// ---------------------------------------------------------------------------

const PROJECT_ID = "proj-resume-001";
const CWD = "/home/me/proj";

function entry(over: Partial<ResumableAgent> = {}): ResumableAgent {
  return {
    agentId: "agent-1",
    name: "Architect",
    nodeId: "node-1",
    conversationId: "conv-1",
    wasRunning: true,
    resumeSupported: true,
    ...over,
  };
}

function renderPanel(seed: ResumableAgent[]) {
  const agent = new MockAgentGateway();
  const layout = new MockLayoutGateway();
  agent._setResumableAgents(PROJECT_ID, seed);

  // Spy on the launch/mutate flow so we assert the exact calls without relying
  // on the singleton/agent-existence checks of the stateful mock.
  const launchSpy = vi
    .spyOn(agent, "launchAgent")
    .mockResolvedValue({} as never);
  // Resolve the layout mutation: the seeded `nodeId`s are synthetic and don't
  // exist in the mock's default tree, so the real `applyOperation` would throw.
  const mutateSpy = vi
    .spyOn(layout, "mutateLayout")
    .mockResolvedValue({} as never);

  const gateways = { agent, layout } as unknown as Gateways;
  const utils = render(
    <DIProvider gateways={gateways}>
      <ResumeProjectPanel projectId={PROJECT_ID} cwd={CWD} />
    </DIProvider>,
  );
  return { agent, layout, launchSpy, mutateSpy, ...utils };
}

describe("ResumeProjectPanel (panel mounting)", () => {
  it("renders nothing when the resumable inventory is empty", async () => {
    const { container } = renderPanel([]);
    // No async inventory ⇒ stays null. Give the effect a tick to settle.
    await waitFor(() => {
      expect(screen.queryByTestId("resume-project-panel")).toBeNull();
    });
    expect(container.firstChild).toBeNull();
  });

  it("mounts the panel listing the resumable agents when non-empty", async () => {
    renderPanel([
      entry({ agentId: "a-run", name: "Architect", wasRunning: true }),
      entry({
        agentId: "a-closed",
        name: "Tester",
        nodeId: "node-2",
        wasRunning: false,
      }),
    ]);

    await screen.findByTestId("resume-project-panel");
    expect(screen.getByTestId("resume-row-a-run")).toBeTruthy();
    expect(screen.getByTestId("resume-row-a-closed")).toBeTruthy();
    // Status derived from wasRunning.
    expect(screen.getByTestId("resume-status-a-run").textContent).toBe(
      "en cours",
    );
    expect(screen.getByTestId("resume-status-a-closed").textContent).toBe(
      "clôt",
    );
  });

  it("uses French labels for the title and global actions", async () => {
    renderPanel([entry()]);
    await screen.findByTestId("resume-project-panel");
    // Title carries a non-breaking space before "?" (`&nbsp;`).
    expect(screen.getByText(/Reprendre les agents\s*\?/)).toBeTruthy();
    expect(screen.getByText("Tout reprendre")).toBeTruthy();
    expect(screen.getByText("Tout ignorer")).toBeTruthy();
    expect(screen.getByText("Nouvelle conversation")).toBeTruthy();
  });
});

describe("ResumeProjectPanel — per-agent actions", () => {
  it("Reprendre (resumeSupported + conversationId) → launchAgent with nodeId + conversationId", async () => {
    const { launchSpy } = renderPanel([
      entry({
        agentId: "a1",
        nodeId: "node-9",
        conversationId: "conv-9",
        resumeSupported: true,
      }),
    ]);
    await screen.findByTestId("resume-project-panel");

    fireEvent.click(screen.getByLabelText("resume Architect"));

    await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(1));
    const [projectId, agentId, options] = launchSpy.mock.calls[0];
    expect(projectId).toBe(PROJECT_ID);
    expect(agentId).toBe("a1");
    expect(options.nodeId).toBe("node-9");
    expect(options.conversationId).toBe("conv-9");
    // The button label is "Reprendre" when resume is supported.
    expect(screen.queryByText("Reprendre")).toBeNull(); // row drained after action
  });

  it("Reprendre (resumeSupported === false) → 'relance à neuf' label + launch WITHOUT conversationId", async () => {
    const { launchSpy } = renderPanel([
      entry({
        agentId: "a2",
        nodeId: "node-7",
        conversationId: "conv-ignored",
        resumeSupported: false,
        name: "Legacy",
      }),
    ]);
    await screen.findByTestId("resume-project-panel");

    // The "relance à neuf" wording is shown for an unsupported profile.
    expect(screen.getByTestId("resume-fresh-note-a2").textContent).toContain(
      "relance à neuf",
    );
    // And the primary button reads "Relancer à neuf" (FR), not "Reprendre".
    const btn = screen.getByLabelText("resume Legacy");
    expect(btn.textContent).toBe("Relancer à neuf");

    fireEvent.click(btn);

    await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(1));
    const [, , options] = launchSpy.mock.calls[0];
    expect(options.nodeId).toBe("node-7");
    // Unsupported ⇒ NO conversation id passed (launches fresh).
    expect(options.conversationId).toBeUndefined();
  });

  it("Nouvelle conversation → setCellConversation(nodeId, null) then launch without conversationId", async () => {
    const { launchSpy, mutateSpy } = renderPanel([
      entry({ agentId: "a3", nodeId: "node-3", conversationId: "conv-3" }),
    ]);
    await screen.findByTestId("resume-project-panel");

    fireEvent.click(screen.getByLabelText("new conversation Architect"));

    await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(1));
    // setCellConversation cleared BEFORE the launch.
    expect(mutateSpy).toHaveBeenCalledWith(PROJECT_ID, {
      type: "setCellConversation",
      target: "node-3",
      conversationId: null,
    });
    const mutateOrder = mutateSpy.mock.invocationCallOrder[0];
    const launchOrder = launchSpy.mock.invocationCallOrder[0];
    expect(mutateOrder).toBeLessThan(launchOrder);
    // Launch carries no conversation id.
    const [, , options] = launchSpy.mock.calls[0];
    expect(options.conversationId).toBeUndefined();
  });

  it("Ignorer → drops the row locally, no launchAgent", async () => {
    const { launchSpy } = renderPanel([
      entry({ agentId: "a4", name: "Doomed" }),
    ]);
    await screen.findByTestId("resume-project-panel");

    fireEvent.click(screen.getByLabelText("ignore Doomed"));

    await waitFor(() =>
      expect(screen.queryByTestId("resume-row-a4")).toBeNull(),
    );
    expect(launchSpy).not.toHaveBeenCalled();
  });
});

describe("ResumeProjectPanel — global actions", () => {
  it("Tout reprendre → launches every pending agent", async () => {
    const { launchSpy } = renderPanel([
      entry({ agentId: "a1", nodeId: "n1" }),
      entry({ agentId: "a2", nodeId: "n2", name: "Tester" }),
    ]);
    await screen.findByTestId("resume-project-panel");

    fireEvent.click(screen.getByLabelText("resume all agents"));

    await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(2));
    const launchedAgentIds = launchSpy.mock.calls.map((c) => c[1]).sort();
    expect(launchedAgentIds).toEqual(["a1", "a2"]);
    // Panel unmounts once the pending set drains.
    await waitFor(() =>
      expect(screen.queryByTestId("resume-project-panel")).toBeNull(),
    );
  });

  it("Tout ignorer → no launch, panel unmounts", async () => {
    const { launchSpy } = renderPanel([
      entry({ agentId: "a1" }),
      entry({ agentId: "a2", name: "Tester" }),
    ]);
    await screen.findByTestId("resume-project-panel");

    fireEvent.click(screen.getByLabelText("ignore all agents"));

    await waitFor(() =>
      expect(screen.queryByTestId("resume-project-panel")).toBeNull(),
    );
    expect(launchSpy).not.toHaveBeenCalled();
  });
});
