/**
 * Agent cell = NATIVE terminal (ARCHITECTURE §20) — supersedes the F2 mediated
 * input contract.
 *
 * The agent cell no longer mediates keystrokes through a separate strip. It is a
 * real terminal:
 *
 *  1. **Keystrokes reach the PTY in BOTH modes.** An agent cell and a plain cell
 *     alike forward `term.onData` keystrokes straight to `handle.write`.
 *  2. **The PTY *output* is always painted** — xterm stays the raw output view.
 *  3. **No `MediatedInput` strip and no `AgentChatView`** are ever mounted; the
 *     agent cell renders only the raw {@link TerminalView}.
 *
 * xterm is stubbed (as in the sibling chat test) so `term.open` succeeds under
 * jsdom and the opener/keystroke wiring genuinely runs. The stub captures the
 * `onData` keystroke handler so the test can fire a keystroke and assert it
 * reached the PTY handle (`handle.write`).
 */
import { beforeEach, describe, it, expect, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";

// Capture the keystroke handler xterm's `onData` is given, and the output sink
// `term.write` so we can assert PTY output is painted. A single shared slot is
// enough: each test renders exactly one terminal cell.
const xtermState: {
  keyHandler: ((data: string) => void) | null;
  writes: Uint8Array[];
} = { keyHandler: null, writes: [] };

vi.mock("@xterm/xterm", () => ({
  Terminal: class {
    loadAddon() {}
    open() {}
    onData(cb: (data: string) => void) {
      xtermState.keyHandler = cb;
      return { dispose() {} };
    }
    onResize() {
      return { dispose() {} };
    }
    write(bytes: Uint8Array) {
      xtermState.writes.push(bytes);
    }
    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, TerminalHandle } from "@/ports";
import {
  MockAgentGateway,
  MockInputGateway,
  MockLayoutGateway,
  MockSystemGateway,
  MockTerminalGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { leaves } from "./layout";
import { LayoutGrid } from "./LayoutGrid";

/** Fresh mock set (input gateway included so the write-portal is fully wired). */
function makeGateways(agentGateway: MockAgentGateway): Gateways {
  return {
    layout: new MockLayoutGateway(),
    agent: agentGateway,
    terminal: new MockTerminalGateway(),
    system: new MockSystemGateway(),
    input: new MockInputGateway(),
  } as unknown as Gateways;
}

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

/**
 * Spy on the handle the agent gateway hands out, so we can detect whether a
 * keystroke reached the PTY (`handle.write`). Returns the spy.
 */
function spyAgentWrite(gw: MockAgentGateway): ReturnType<typeof vi.fn> {
  const writeSpy = vi.fn();
  const original = gw.launchAgent.bind(gw);
  vi.spyOn(gw, "launchAgent").mockImplementation(async (p, a, o, onData) => {
    const handle: TerminalHandle = await original(p, a, o, onData);
    return { ...handle, write: writeSpy };
  });
  return writeSpy;
}

/** Spy on the terminal gateway handle write (plain-cell PTY path). */
function spyTerminalWrite(gw: MockTerminalGateway): ReturnType<typeof vi.fn> {
  const writeSpy = vi.fn();
  const original = gw.openTerminal.bind(gw);
  vi.spyOn(gw, "openTerminal").mockImplementation(async (o, onData) => {
    const handle: TerminalHandle = await original(o, onData);
    return { ...handle, write: writeSpy };
  });
  return writeSpy;
}

async function pinAgent(gateways: Gateways, agentId: string): Promise<void> {
  const layout = gateways.layout as MockLayoutGateway;
  const leafId = leaves(await layout.loadLayout("p1"))[0].id;
  await layout.mutateLayout("p1", {
    type: "setCellAgent",
    target: leafId,
    agent: agentId,
  });
}

beforeEach(() => {
  xtermState.keyHandler = null;
  xtermState.writes = [];
});

describe("LayoutGrid — agent cell is a native terminal (§20)", () => {
  it("an AGENT cell DOES write keystrokes to the PTY (native terminal)", async () => {
    const agentGateway = new MockAgentGateway();
    const agent = await agentGateway.createAgent("p1", {
      name: "Worker",
      profileId: "claude",
    });
    const gateways = makeGateways(agentGateway);
    await pinAgent(gateways, agent.id);

    const writeSpy = spyAgentWrite(agentGateway);

    renderGrid(gateways);

    await waitFor(() => expect(agentGateway.launchAgent).toHaveBeenCalled());
    await waitFor(() => expect(xtermState.keyHandler).not.toBeNull());

    // Simulate human keystrokes into xterm; in native mode they reach the PTY.
    xtermState.keyHandler!("h");
    xtermState.keyHandler!("i");

    await waitFor(() => expect(writeSpy).toHaveBeenCalled());
  });

  it("a PLAIN cell still writes keystrokes to the PTY (raw shell, unchanged)", async () => {
    const agentGateway = new MockAgentGateway();
    const gateways = makeGateways(agentGateway);
    const writeSpy = spyTerminalWrite(gateways.terminal as MockTerminalGateway);

    renderGrid(gateways);

    await waitFor(() =>
      expect((gateways.terminal as MockTerminalGateway).openTerminal).toHaveBeenCalled(),
    );
    await waitFor(() => expect(xtermState.keyHandler).not.toBeNull());

    xtermState.keyHandler!("l");
    xtermState.keyHandler!("s");

    await waitFor(() => expect(writeSpy).toHaveBeenCalled());
  });

  it("paints PTY output in BOTH modes (xterm output view unchanged)", async () => {
    const agentGateway = new MockAgentGateway();
    const agent = await agentGateway.createAgent("p1", {
      name: "Worker",
      profileId: "claude",
    });
    const gateways = makeGateways(agentGateway);
    await pinAgent(gateways, agent.id);

    renderGrid(gateways);

    await waitFor(() => expect(xtermState.writes.length).toBeGreaterThan(0));
  });

  it("renders only the raw TerminalView — no MediatedInput, no AgentChatView", async () => {
    const agentGateway = new MockAgentGateway();
    const agent = await agentGateway.createAgent("p1", {
      name: "Worker",
      profileId: "claude",
    });
    const gateways = makeGateways(agentGateway);
    await pinAgent(gateways, agent.id);

    renderGrid(gateways);

    await waitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
    expect(screen.getByTestId("terminal-view")).toBeTruthy();
    expect(screen.queryByTestId("mediated-input")).toBeNull();
    expect(screen.queryByTestId("agent-chat-view")).toBeNull();
  });

  it("a PLAIN cell has no portal overlay", async () => {
    const agentGateway = new MockAgentGateway();
    const gateways = makeGateways(agentGateway);

    renderGrid(gateways);

    await waitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
    expect(screen.getByTestId("terminal-view")).toBeTruthy();
    expect(screen.queryByTestId("write-portal-overlay")).toBeNull();
  });
});
