/**
 * R0d — launch-path mapping of the backend singleton refusal.
 *
 * When `launch_agent` is refused with `AGENT_ALREADY_RUNNING` (the agent is a
 * singleton already alive in another cell), the hosting leaf must surface a
 * clear notice — never a raw error — together with an "aller à la cellule"
 * action that focuses the live host. The host cell is resolved from the live
 * agents set (the R0b source of truth: PTY *and* chat), not by parsing the
 * error message.
 *
 * The terminal opener that drives the launch only runs once xterm's `open`
 * succeeds; under jsdom the real xterm bails, so — exactly like
 * `LayoutGrid.chat.test.tsx` — we stub *only* xterm (the headless obstacle),
 * leaving the leaf's launch + error-mapping logic untouched.
 */
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";

// Minimal xterm stub so `term.open` succeeds and the opener (→ launchAgent) runs.
vi.mock("@xterm/xterm", () => ({
  Terminal: class {
    loadAddon() {}
    open() {}
    onData() {
      return { dispose() {} };
    }
    onResize() {
      return { dispose() {} };
    }
    write() {}
    dispose() {}
    get cols() {
      return 80;
    }
    get rows() {
      return 24;
    }
  },
}));
vi.mock("@xterm/addon-fit", () => ({
  FitAddon: class {
    fit() {}
  },
}));
vi.mock("@xterm/xterm/css/xterm.css", () => ({}));

if (typeof globalThis.ResizeObserver === "undefined") {
  globalThis.ResizeObserver = class {
    observe() {}
    unobserve() {}
    disconnect() {}
  } as unknown as typeof ResizeObserver;
}

import type { Gateways } from "@/ports";
import {
  MockAgentGateway,
  MockLayoutGateway,
  MockSystemGateway,
  MockTerminalGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { leaves } from "./layout";
import { LayoutGrid } from "./LayoutGrid";

function renderGrid(gateways: Gateways) {
  return render(
    <DIProvider gateways={gateways}>
      <LayoutGrid projectId="p1" cwd="/home/me/proj" />
    </DIProvider>,
  );
}

describe("R0d — launch refused with AGENT_ALREADY_RUNNING", () => {
  it("maps the refusal to a clear notice + 'aller à la cellule' on the host", async () => {
    const layout = new MockLayoutGateway();
    const agentGateway = new MockAgentGateway();
    const terminal = new MockTerminalGateway();
    const system = new MockSystemGateway();

    const agent = await agentGateway.createAgent("p1", {
      name: "Solo",
      profileId: "claude",
    });

    // Two visible leaves; pin the agent on cell A.
    const initial = await layout.loadLayout("p1");
    const aId = leaves(initial)[0].id;
    await layout.mutateLayout("p1", {
      type: "split",
      target: aId,
      direction: "row",
      newLeaf: "b",
      container: "c",
    });
    const after = leaves(await layout.loadLayout("p1"));
    const bId = after.find((l) => l.id !== aId)!.id;
    await layout.mutateLayout("p1", {
      type: "setCellAgent",
      target: aId,
      agent: agent.id,
    });

    // The live set reports the agent already hosted in cell B (the host).
    vi.spyOn(agentGateway, "listLiveAgents").mockResolvedValue([
      { agentId: agent.id, nodeId: bId, sessionId: "s-1", kind: "pty" },
    ]);
    // The launch from cell A is refused by the backend (singleton invariant).
    vi.spyOn(agentGateway, "launchAgent").mockRejectedValue({
      code: "AGENT_ALREADY_RUNNING",
      message: `agent ${agent.id} is already running in cell ${bId}`,
    });

    const gateways = {
      layout,
      agent: agentGateway,
      terminal,
      system,
    } as unknown as Gateways;
    renderGrid(gateways);

    // The leaf's terminal opener runs the launch → refused → mapped to a notice.
    const status = await screen.findByRole("status");
    expect(status.textContent).toMatch(/déjà actif dans une autre cellule/);
    // And NOT a raw error dump.
    expect(status.textContent).not.toMatch(/AGENT_ALREADY_RUNNING/);

    // The go-to action focuses the live host cell (B): it gets an outline flash.
    const goTo = await screen.findByRole("button", {
      name: "aller à la cellule",
    });
    fireEvent.click(goTo);
    await waitFor(() => {
      const host = document.querySelector<HTMLElement>(
        `[data-node-id="${bId}"]`,
      )!;
      expect(host.style.outline).not.toBe("");
    });
  });
});
