import { describe, expect, test } from "bun:test";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createSessionsAdapter } from "../adapters/sessions";
import { Journal } from "../journal";
import { CollectorState } from "../state";
import type { LedgerReadResult, SessionRecord } from "./ledger";
import { readSessionScreen, writeSessionKeys } from "./screen";
import type { TmuxCommandResult } from "./tmux";
import type { BuildboxRegistry } from "../buildbox-registry";
import type { HostFacts } from "./hosts";

function record(overrides: Partial<SessionRecord> = {}): SessionRecord {
  return {
    id: "s1",
    runtime: "claude",
    pid: 100,
    cwd: "/work",
    repo: "overdeck",
    repoRoot: "/work",
    branch: "main",
    worktree: "/work",
    host: "e14",
    startedAt: "2026-08-07T03:00:00.000Z",
    lastHeartbeatAt: "2026-08-07T03:30:00.000Z",
    lastProgressAt: "2026-08-07T03:30:00.000Z",
    finishedAt: null,
    finishReason: null,
    transcriptPath: "/t.jsonl",
    tmuxSession: "agent-s1",
    tmuxSocket: "/run/tmux/sock",
    mux: null,
    attached: false,
    evidence: null,
    lastActiveAt: "2026-08-07T03:30:00.000Z",
    parent: null,
    parentLedgerId: null,
    state: "DETACHED-ALIVE",
    launchedBy: null,
    title: null,
    activity: null,
    account: null,
    ...overrides,
  };
}

async function stateWithSessions(
  sessions: SessionRecord[],
  attachAllowed = true,
  hosts: HostFacts[] = [],
): Promise<CollectorState> {
  const state = new CollectorState(new Journal(join(mkdtempSync(join(tmpdir(), "overdeck-screen-")), "journal.jsonl")));
  const adapter = createSessionsAdapter({
    attachAllowed,
    attachBlockedReason: "collector is bound to 100.64.0.2, not loopback — browser attach is refused off-host",
    readLedgerImpl: (): LedgerReadResult => ({ sessions, dir: "/ledger", missing: false, unreadable: [] }),
    tmuxBinaryImpl: () => "/usr/bin/tmux",
    readHostRegistryImpl: () => ({ hosts, missing: false }),
    hostnameImpl: () => "e14",
    probeRemoteImpl: async () => [],
  });
  state.registerAdapter(adapter.id, adapter.interval);
  const result = await adapter.poll();
  state.recordSuccess(adapter.id, Date.now(), result.items, result.panels);
  return state;
}

function registry(host = "debian1", state: "reachable" | "unreachable" | "bricked" = "reachable"): BuildboxRegistry {
  return {
    schema_version: 1,
    source: "/registry.json",
    orders: {},
    hosts: [{ name: host, ssh_alias: `${host}-alias`, state, machine_id: null, roles: [], access: {}, rustdesk: null, notes: "" }],
  };
}

function spawnStub(result: Partial<TmuxCommandResult>, seen: string[][] = []) {
  return async (argv: string[]): Promise<TmuxCommandResult> => {
    seen.push(argv);
    return { rc: 0, stdout: "", stderr: "", ...result };
  };
}

describe("session screen", () => {
  test("captures the pane of an attachable session", async () => {
    const state = await stateWithSessions([record()]);
    const seen: string[][] = [];
    const response = await readSessionScreen(state, "s1", {
      binary: () => "/usr/bin/tmux",
      spawn: spawnStub({ stdout: "[32mready[0m" }, seen),
    });
    expect(response.status).toBe(200);
    expect(response.body.screen).toBe("[32mready[0m");
    expect(seen[0]).toEqual([
      "/usr/bin/tmux", "-S", "/run/tmux/sock", "capture-pane", "-p", "-e", "-t", "=agent-s1",
    ]);
  });

  test("an unknown session id is refused", async () => {
    const state = await stateWithSessions([record()]);
    const response = await readSessionScreen(state, "nope", {
      binary: () => "/usr/bin/tmux",
      spawn: spawnStub({}),
    });
    expect(response.status).toBe(404);
    expect(response.body.ok).toBe(false);
  });

  test("an orphaned session cannot be attached", async () => {
    const state = await stateWithSessions([record({ state: "ORPHANED" })]);
    const response = await readSessionScreen(state, "s1", {
      binary: () => "/usr/bin/tmux",
      spawn: spawnStub({}),
    });
    expect(response.status).toBe(409);
    expect(response.body.error).toBe("session is ORPHANED");
  });

  test("a non-loopback bind refuses capture", async () => {
    const state = await stateWithSessions([record()], false);
    const response = await readSessionScreen(state, "s1", {
      binary: () => "/usr/bin/tmux",
      spawn: spawnStub({}),
    });
    expect(response.status).toBe(409);
    expect(response.body.error).toContain("not loopback");
  });

  test("a missing tmux binary reports unavailable instead of failing opaquely", async () => {
    const state = await stateWithSessions([record()]);
    const response = await readSessionScreen(state, "s1", { binary: () => null });
    expect(response.status).toBe(503);
    expect(response.body.error).toBe("tmux is not installed on this host");
  });

  test("keystrokes are sent literally to the ledger's tmux target", async () => {
    const state = await stateWithSessions([record()]);
    const seen: string[][] = [];
    const sent = await writeSessionKeys(state, "s1", { text: "-rf continue" }, {
      binary: () => "/usr/bin/tmux",
      spawn: spawnStub({}, seen),
    });
    expect(sent.status).toBe(200);
    expect(seen[0]).toEqual([
      "/usr/bin/tmux", "-S", "/run/tmux/sock", "send-keys", "-t", "=agent-s1", "-l", "--", "-rf continue",
    ]);
  });

  test("named keys are sent without the literal flag", async () => {
    const state = await stateWithSessions([record()]);
    const seen: string[][] = [];
    await writeSessionKeys(state, "s1", { key: "C-c" }, {
      binary: () => "/usr/bin/tmux",
      spawn: spawnStub({}, seen),
    });
    expect(seen[0]!.slice(-3)).toEqual(["=agent-s1", "--", "C-c"]);
  });

  test("a tmux failure surfaces its stderr rather than reporting success", async () => {
    const state = await stateWithSessions([record()]);
    const sent = await writeSessionKeys(state, "s1", { key: "Enter" }, {
      binary: () => "/usr/bin/tmux",
      spawn: spawnStub({ rc: 1, stderr: "can't find session" }),
    });
    expect(sent.status).toBe(502);
    expect(sent.result).toBe("can't find session");
  });

  test("captures a reachable remote pane through registry-resolved ssh with quoted target", async () => {
    const state = await stateWithSessions(
      [record({ host: "debian1", tmuxSocket: "/run/tmux/socket with space", tmuxSession: "agent's pane" })],
      true,
      [{ name: "debian1", reachability: "declared-enabled", state: "reachable", notes: null }],
    );
    const seen: string[][] = [];
    const response = await readSessionScreen(state, "s1", {
      loadRegistry: async () => registry(),
      spawn: spawnStub({ stdout: "remote screen" }, seen),
    });
    expect(response.status).toBe(200);
    expect(response.body.screen).toBe("remote screen");
    expect(seen[0]).toEqual([
      "ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=4", "-o", "StrictHostKeyChecking=accept-new",
      "debian1-alias",
      "'tmux' '-S' '/run/tmux/socket with space' 'capture-pane' '-p' '-e' '-t' '=agent'\\''s pane'",
    ]);
  });

  test("surfaces a remote ssh failure without fabricating a screen", async () => {
    const state = await stateWithSessions(
      [record({ host: "debian1" })], true,
      [{ name: "debian1", reachability: "declared-enabled", state: "reachable", notes: null }],
    );
    const response = await readSessionScreen(state, "s1", {
      loadRegistry: async () => registry(),
      spawn: spawnStub({ rc: 255, stderr: "ssh: connect to host debian1: No route to host" }),
    });
    expect(response.status).toBe(502);
    expect(response.body.screen).toBe("");
    expect(response.body.error).toContain("No route to host");
  });

  test("sendKeys uses the same remote ssh path", async () => {
    const state = await stateWithSessions(
      [record({ host: "debian1" })], true,
      [{ name: "debian1", reachability: "declared-enabled", state: "reachable", notes: null }],
    );
    const seen: string[][] = [];
    const sent = await writeSessionKeys(state, "s1", { text: "hello world" }, {
      loadRegistry: async () => registry(),
      spawn: spawnStub({}, seen),
    });
    expect(sent.status).toBe(200);
    expect(seen[0]!.at(-1)).toContain("'send-keys' '-t' '=agent-s1' '-l' '--' 'hello world'");
  });
});
