/**
 * Ticket #147 — custom CLI mode for structured/headless agent cells.
 *
 * Plain cells and PTY-only profiles stay on the native TUI. Structured profiles
 * get a per-cell toggle; switching a live session requires confirmation, then
 * the custom chat view drives `ReplyChunk` streams defensively.
 */
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";

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 { AgentProfile } from "@/domain";
import type { Gateways } from "@/ports";
import {
  MockAgentGateway,
  MockLayoutGateway,
  MockProfileGateway,
  MockSystemGateway,
  MockTerminalGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { leaves } from "./layout";
import { LayoutGrid } from "./LayoutGrid";

const structuredProfile: AgentProfile = {
  id: "mock-structured",
  name: "Structured Codex",
  command: "codex",
  args: [],
  contextInjection: { strategy: "conventionFile", target: "AGENTS.md" },
  detect: null,
  cwdTemplate: "{projectRoot}",
  structuredAdapter: "codex",
};

const ptyProfile: AgentProfile = {
  id: "mock-pty",
  name: "Plain PTY",
  command: "bash",
  args: [],
  contextInjection: { strategy: "conventionFile", target: "AGENTS.md" },
  detect: null,
  cwdTemplate: "{projectRoot}",
};

function deferred<T>() {
  let resolve!: (value: T | PromiseLike<T>) => void;
  const promise = new Promise<T>((res) => {
    resolve = res;
  });
  return { promise, resolve };
}

beforeEach(() => {
  window.localStorage.clear();
});

async function seeded(profile: AgentProfile): Promise<Gateways> {
  const layout = new MockLayoutGateway();
  const agent = new MockAgentGateway();
  const profileGateway = new MockProfileGateway();
  const terminal = new MockTerminalGateway();
  const system = new MockSystemGateway();
  await profileGateway.configureProfiles([profile]);
  const created = await agent.createAgent("p1", {
    name: "Worker",
    profileId: profile.id,
  });
  const tree = await layout.loadLayout("p1");
  const leafId = leaves(tree)[0].id;
  await layout.mutateLayout("p1", {
    type: "setCellAgent",
    target: leafId,
    agent: created.id,
  });
  return {
    layout,
    agent,
    profile: profileGateway,
    terminal,
    system,
  } as unknown as Gateways;
}

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

describe("LayoutGrid custom agent CLI (#147)", () => {
  it("does not show the custom CLI toggle in a plain cell", async () => {
    const gateways = {
      layout: new MockLayoutGateway(),
      agent: new MockAgentGateway(),
      profile: new MockProfileGateway(),
      terminal: new MockTerminalGateway(),
      system: new MockSystemGateway(),
    } as unknown as Gateways;

    renderGrid(gateways);

    await waitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
    expect(screen.queryByText("CLI custom")).toBeNull();
    expect(screen.getByTestId("terminal-view")).toBeTruthy();
  });

  it("does not show the custom CLI toggle for a PTY-only profile", async () => {
    renderGrid(await seeded(ptyProfile));

    await waitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
    expect(screen.queryByText("CLI custom")).toBeNull();
    expect(screen.getByTestId("terminal-view")).toBeTruthy();
  });

  it("shows the custom CLI toggle for structured profiles and streams a final callout", async () => {
    renderGrid(await seeded(structuredProfile));

    await waitFor(() => expect(screen.getByText("CLI custom")).toBeTruthy());
    expect(screen.getByTestId("terminal-view")).toBeTruthy();

    fireEvent.click(screen.getByText("CLI custom"));
    await waitFor(() =>
      expect(screen.getByRole("alertdialog", { name: "Confirmer le changement de CLI" })).toBeTruthy(),
    );
    fireEvent.click(screen.getByText("Arrêter et relancer"));
    await waitFor(() =>
      expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy(),
    );

    fireEvent.click(screen.getByText("Joindre"));
    await waitFor(() =>
      expect(screen.getByText(/mock-attachment.txt/)).toBeTruthy(),
    );
    fireEvent.change(screen.getByLabelText(/message CLI custom/), {
      target: { value: "hello there" },
    });
    fireEvent.click(screen.getByText("Envoyer"));

    await waitFor(() => expect(screen.getByText("Task Complete")).toBeTruthy());
    expect(screen.getAllByText(/Agent: reçu/).length).toBeGreaterThan(0);
    expect(screen.getAllByText(/hello there/).length).toBeGreaterThan(0);
    expect(screen.getAllByText(/mock-attachment.txt/).length).toBeGreaterThan(0);
  });

  it("requires confirmation before switching a live native TUI session", async () => {
    renderGrid(await seeded(structuredProfile));

    await waitFor(() => expect(screen.getByText("CLI custom")).toBeTruthy());
    await waitFor(() =>
      expect(screen.getByTestId("terminal-view")).toBeTruthy(),
    );

    fireEvent.click(screen.getByText("CLI custom"));

    await waitFor(() =>
      expect(screen.getByRole("alertdialog", { name: "Confirmer le changement de CLI" })).toBeTruthy(),
    );
    expect(screen.getByText(/Une reprise est possible/)).toBeTruthy();

    fireEvent.click(screen.getByText("Arrêter et relancer"));
    await waitFor(() =>
      expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy(),
    );
  });

  it("keeps a restored custom CLI mode while the agent/profile catalog is still loading (#149)", async () => {
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const profileGateway = new MockProfileGateway();
    const terminal = new MockTerminalGateway();
    const system = new MockSystemGateway();
    await profileGateway.configureProfiles([structuredProfile]);
    const created = await agent.createAgent("p1", {
      name: "Worker",
      profileId: structuredProfile.id,
    });
    const tree = await layout.loadLayout("p1");
    const leafId = leaves(tree)[0].id;
    await layout.mutateLayout("p1", {
      type: "setCellAgent",
      target: leafId,
      agent: created.id,
    });
    window.localStorage.setItem(`idea.agent-cell-mode.p1.${leafId}`, "custom");

    const agentsLoaded = deferred<void>();
    const profilesLoaded = deferred<void>();
    const originalListAgents = agent.listAgents.bind(agent);
    const originalListProfiles = profileGateway.listProfiles.bind(profileGateway);
    vi.spyOn(agent, "listAgents").mockImplementation(async (projectId) => {
      await agentsLoaded.promise;
      return originalListAgents(projectId);
    });
    vi.spyOn(profileGateway, "listProfiles").mockImplementation(async () => {
      await profilesLoaded.promise;
      return originalListProfiles();
    });

    renderGrid({
      layout,
      agent,
      profile: profileGateway,
      terminal,
      system,
    } as unknown as Gateways);

    await waitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
    expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom");
    expect(screen.queryByTestId("custom-agent-chat-view")).toBeNull();

    agentsLoaded.resolve();
    profilesLoaded.resolve();

    await waitFor(() =>
      expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy(),
    );
    expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom");
  });

  it("falls back to native TUI once the catalog confirms the pinned profile is incompatible", async () => {
    const layout = new MockLayoutGateway();
    const agent = new MockAgentGateway();
    const profileGateway = new MockProfileGateway();
    const terminal = new MockTerminalGateway();
    const system = new MockSystemGateway();
    await profileGateway.configureProfiles([ptyProfile]);
    const created = await agent.createAgent("p1", {
      name: "Worker",
      profileId: ptyProfile.id,
    });
    const tree = await layout.loadLayout("p1");
    const leafId = leaves(tree)[0].id;
    await layout.mutateLayout("p1", {
      type: "setCellAgent",
      target: leafId,
      agent: created.id,
    });
    window.localStorage.setItem(`idea.agent-cell-mode.p1.${leafId}`, "custom");

    const agentsLoaded = deferred<void>();
    const profilesLoaded = deferred<void>();
    const originalListAgents = agent.listAgents.bind(agent);
    const originalListProfiles = profileGateway.listProfiles.bind(profileGateway);
    vi.spyOn(agent, "listAgents").mockImplementation(async (projectId) => {
      await agentsLoaded.promise;
      return originalListAgents(projectId);
    });
    vi.spyOn(profileGateway, "listProfiles").mockImplementation(async () => {
      await profilesLoaded.promise;
      return originalListProfiles();
    });

    renderGrid({
      layout,
      agent,
      profile: profileGateway,
      terminal,
      system,
    } as unknown as Gateways);

    await waitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
    expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom");

    agentsLoaded.resolve();
    profilesLoaded.resolve();

    await waitFor(() =>
      expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("tui"),
    );
    expect(screen.getByTestId("terminal-view")).toBeTruthy();
    expect(screen.queryByTestId("custom-agent-chat-view")).toBeNull();
  });
});
