/**
 * "One live session per agent" invariant (UI side).
 *
 * - The mock agent gateway mirrors the backend guard: launching an agent already
 *   live in another cell is refused with `AGENT_ALREADY_RUNNING`, and the same
 *   node is idempotent; `listLiveAgents` reports who runs where (T5).
 * - The per-cell dropdown blocks an agent already visible in another cell, but
 *   can rebind a background live session when the backend exposes its session id.
 */
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";

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

const noop = () => {};

describe("singleton agent — mock gateway guard (T5)", () => {
  it("refuses launching an agent already running in another cell", async () => {
    const agent = new MockAgentGateway();
    const a = await agent.createAgent("p1", { name: "Solo", profileId: "p" });

    // Launch in cell A.
    await agent.launchAgent(
      "p1",
      a.id,
      { cwd: "/x", rows: 24, cols: 80, nodeId: "node-A" },
      noop,
    );

    // Launching the SAME agent in cell B is refused.
    let caught: GatewayError | null = null;
    await agent
      .launchAgent(
        "p1",
        a.id,
        { cwd: "/x", rows: 24, cols: 80, nodeId: "node-B" },
        noop,
      )
      .catch((e) => {
        caught = e as GatewayError;
      });
    expect(caught).not.toBeNull();
    expect(caught!.code).toBe("AGENT_ALREADY_RUNNING");
  });

  it("listLiveAgents reports the cell hosting each live agent", async () => {
    const agent = new MockAgentGateway();
    const a = await agent.createAgent("p1", { name: "Solo", profileId: "p" });

    expect(await agent.listLiveAgents("p1")).toEqual([]);

    await agent.launchAgent(
      "p1",
      a.id,
      { cwd: "/x", rows: 24, cols: 80, nodeId: "node-A" },
      noop,
    );

    const live: LiveAgent[] = await agent.listLiveAgents("p1");
    expect(live).toEqual([
      {
        agentId: a.id,
        nodeId: "node-A",
        sessionId: "mock-agent-session-1",
      },
    ]);
  });

  it("relaunching in the SAME node is allowed (idempotent), not refused", async () => {
    const agent = new MockAgentGateway();
    const a = await agent.createAgent("p1", { name: "Solo", profileId: "p" });

    await agent.launchAgent(
      "p1",
      a.id,
      { cwd: "/x", rows: 24, cols: 80, nodeId: "node-A" },
      noop,
    );
    // Same node again must NOT throw.
    await expect(
      agent.launchAgent(
        "p1",
        a.id,
        { cwd: "/x", rows: 24, cols: 80, nodeId: "node-A" },
        noop,
      ),
    ).resolves.toBeDefined();
  });
});

describe("singleton agent — dropdown guard (T6)", () => {
  function renderGrid(layout: MockLayoutGateway, agent: MockAgentGateway) {
    // A system gateway stub so LeafView's event subscription is a no-op.
    const system = {
      onDomainEvent: vi.fn().mockResolvedValue(() => {}),
    };
    const gateways = { layout, agent, system } as unknown as Gateways;
    return render(
      <DIProvider gateways={gateways}>
        <LayoutGrid projectId="p1" cwd="/home/me/proj" />
      </DIProvider>,
    );
  }

  it("opens an agent already running elsewhere by rebinding its live session", async () => {
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const a = await agent.createAgent("p1", { name: "Busy", profileId: "p" });
    await agent.launchAgent(
      "p1",
      a.id,
      { cwd: "/x", rows: 24, cols: 80, nodeId: "old-node" },
      noop,
    );

    const tree = await layout.loadLayout("p1");
    const leafId = leaves(tree)[0].id;

    renderGrid(layout, agent);

    // The option is selectable: choosing it rebinds the running session here.
    const option = await screen.findByRole("option", {
      name: /Busy/,
    });
    expect((option as HTMLOptionElement).disabled).toBe(false);

    const select = screen.getByRole("combobox") as HTMLSelectElement;
    fireEvent.change(select, { target: { value: a.id } });

    await waitFor(async () => {
      const updated = await layout.loadLayout("p1");
      const leaf = leaves(updated).find((l) => l.id === leafId)!;
      expect(leaf.agent).toBe(a.id);
      expect(leaf.session).toBe("mock-agent-session-1");
    });
    expect(await agent.listLiveAgents("p1")).toEqual([
      {
        agentId: a.id,
        nodeId: leafId,
        sessionId: "mock-agent-session-1",
      },
    ]);
  });

  it("explains the missing backend contract when a live agent has no attachable session", async () => {
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const a = await agent.createAgent("p1", { name: "Busy", profileId: "p" });

    vi.spyOn(agent, "listLiveAgents").mockResolvedValue([
      { agentId: a.id, nodeId: "some-other-node" },
    ]);

    renderGrid(layout, agent);

    const option = await screen.findByRole("option", {
      name: /Busy/,
    });
    expect((option as HTMLOptionElement).disabled).toBe(false);

    const select = screen.getByRole("combobox") as HTMLSelectElement;
    fireEvent.change(select, { target: { value: a.id } });

    expect((await screen.findByRole("status")).textContent).toMatch(
      /Session active introuvable/,
    );
  });

  it("the agent pinned on THIS cell stays selectable even while live (same node)", async () => {
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const a = await agent.createAgent("p1", { name: "Mine", profileId: "p" });

    // Pin the agent on the single leaf.
    const tree = await layout.loadLayout("p1");
    const leafId = leaves(tree)[0].id;
    await layout.mutateLayout("p1", {
      type: "setCellAgent",
      target: leafId,
      agent: a.id,
    });

    // The agent is live in THIS very cell (same node id as the leaf).
    vi.spyOn(agent, "listLiveAgents").mockResolvedValue([
      { agentId: a.id, nodeId: leafId },
    ]);

    renderGrid(layout, agent);

    const option = await screen.findByRole("option", { name: "Mine" });
    // Live on its own cell ⇒ NOT disabled (it can keep running here).
    expect((option as HTMLOptionElement).disabled).toBe(false);
  });
});
