/**
 * L3 — TerminalView agent cell is a **native terminal** wired to a
 * {@link WritePortal} (ARCHITECTURE §20).
 *
 * Contracts exercised (xterm stubbed so `term.open` succeeds under jsdom and the
 * keystroke wiring genuinely runs; the stub captures the `onData` handler):
 *  - In agent mode a keystroke is written to the PTY (native), AND reported to
 *    the portal for line counting.
 *  - The portal's `isSuspended()` gates the relay: while suspended, keystrokes
 *    are NOT forwarded to the PTY (but are still reported for counting).
 *  - The live handle is bound to the portal on adopt, unbound on unmount.
 */
import { beforeEach, describe, it, expect, vi } from "vitest";
import { render, waitFor } from "@testing-library/react";

const xtermState: { keyHandler: ((data: string) => void) | null } = {
  keyHandler: null,
};

vi.mock("@xterm/xterm", () => ({
  Terminal: class {
    loadAddon() {}
    open() {}
    onData(cb: (data: string) => void) {
      xtermState.keyHandler = cb;
      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, TerminalHandle, WritePortal } from "@/ports";
import { DIProvider } from "@/app/di";
import { TerminalView } from "./TerminalView";

function makeHandle(write = vi.fn().mockResolvedValue(undefined)): TerminalHandle {
  return {
    sessionId: "s1",
    write,
    resize: vi.fn().mockResolvedValue(undefined),
    detach: vi.fn(),
    close: vi.fn().mockResolvedValue(undefined),
  };
}

function makePortal(overrides: Partial<WritePortal> = {}): WritePortal {
  return {
    onHumanData: vi.fn(),
    isSuspended: vi.fn(() => false),
    bindHandle: vi.fn(),
    unbindHandle: vi.fn(),
    ...overrides,
  };
}

function decode(b: Uint8Array): string {
  return new TextDecoder().decode(b);
}

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

describe("TerminalView native agent terminal + portal (§20)", () => {
  it("writes keystrokes to the PTY in agent mode and reports them to the portal", async () => {
    const write = vi.fn().mockResolvedValue(undefined);
    const handle = makeHandle(write);
    const open = vi.fn(async () => handle);
    const portal = makePortal();
    const gateways = { terminal: { reattach: vi.fn() } } as unknown as Gateways;

    render(
      <DIProvider gateways={gateways}>
        <TerminalView cwd="/c" open={open} agentMode portal={portal} />
      </DIProvider>,
    );

    await waitFor(() => expect(xtermState.keyHandler).not.toBeNull());
    await waitFor(() => expect(portal.bindHandle).toHaveBeenCalledWith(handle));

    xtermState.keyHandler!("a");

    expect(portal.onHumanData).toHaveBeenCalledWith("a");
    await waitFor(() => expect(write).toHaveBeenCalled());
    expect(decode(write.mock.calls[0][0])).toBe("a");
  });

  it("does NOT forward keystrokes while the portal is suspended (but still counts them)", async () => {
    const write = vi.fn().mockResolvedValue(undefined);
    const handle = makeHandle(write);
    const open = vi.fn(async () => handle);
    const onHumanData = vi.fn();
    const portal = makePortal({ isSuspended: () => true, onHumanData });
    const gateways = { terminal: { reattach: vi.fn() } } as unknown as Gateways;

    render(
      <DIProvider gateways={gateways}>
        <TerminalView cwd="/c" open={open} agentMode portal={portal} />
      </DIProvider>,
    );

    await waitFor(() => expect(xtermState.keyHandler).not.toBeNull());
    await waitFor(() => expect(portal.bindHandle).toHaveBeenCalled());

    xtermState.keyHandler!("x");

    // Reported for counting, but the relay is suspended ⇒ not written.
    expect(onHumanData).toHaveBeenCalledWith("x");
    expect(write).not.toHaveBeenCalled();
  });

  it("unbinds the handle from the portal on unmount", async () => {
    const handle = makeHandle();
    const open = vi.fn(async () => handle);
    const portal = makePortal();
    const gateways = { terminal: { reattach: vi.fn() } } as unknown as Gateways;

    const { unmount } = render(
      <DIProvider gateways={gateways}>
        <TerminalView cwd="/c" open={open} agentMode portal={portal} />
      </DIProvider>,
    );

    await waitFor(() => expect(portal.bindHandle).toHaveBeenCalled());
    unmount();
    expect(portal.unbindHandle).toHaveBeenCalled();
  });

  it("a PLAIN cell ignores the portal and writes keystrokes natively", async () => {
    const write = vi.fn().mockResolvedValue(undefined);
    const handle = makeHandle(write);
    const open = vi.fn(async () => handle);
    const portal = makePortal();
    const gateways = { terminal: { reattach: vi.fn() } } as unknown as Gateways;

    render(
      <DIProvider gateways={gateways}>
        {/* agentMode false: portal must never be touched */}
        <TerminalView cwd="/c" open={open} portal={portal} />
      </DIProvider>,
    );

    await waitFor(() => expect(xtermState.keyHandler).not.toBeNull());
    xtermState.keyHandler!("l");

    await waitFor(() => expect(write).toHaveBeenCalled());
    expect(portal.onHumanData).not.toHaveBeenCalled();
    expect(portal.bindHandle).not.toHaveBeenCalled();
  });
});
