/**
 * "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 disables (and `onChange` rejects) an agent already live
 *   in another cell, while the agent pinned on its own cell stays selectable (T6).
 */
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } 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" }]);
  });

  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("disables an agent already running in another cell and rejects selecting it", async () => {
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const a = await agent.createAgent("p1", { name: "Busy", profileId: "p" });

    // Mark the agent live in a DIFFERENT node than the (single) leaf we render.
    vi.spyOn(agent, "listLiveAgents").mockResolvedValue([
      { agentId: a.id, nodeId: "some-other-node" },
    ]);

    renderGrid(layout, agent);

    // The option for the live-elsewhere agent is rendered disabled with a label.
    const option = await screen.findByRole("option", {
      name: /Busy \(en cours ailleurs\)/,
    });
    expect((option as HTMLOptionElement).disabled).toBe(true);

    // Programmatically selecting it must be rejected (agent not pinned).
    const select = screen.getByRole("combobox") as HTMLSelectElement;
    const setCellAgentSpy = vi.spyOn(layout, "mutateLayout");
    fireEvent.change(select, { target: { value: a.id } });

    // A notice is shown and no setCellAgent mutation was issued.
    expect(await screen.findByRole("status")).toBeTruthy();
    expect(
      setCellAgentSpy.mock.calls.filter((c) => c[1].type === "setCellAgent"),
    ).toHaveLength(0);
  });

  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);
  });
});
