/**
 * @file pty-run.test.js
 * @description Unit tests for the tmux-backed run lifecycle. Injects a fake
 * tmux exec implementation (via tmux.js's test seam) so no real tmux binary
 * is invoked.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */
const { describe, it, beforeEach, before, after } = require("node:test");
const assert = require("node:assert/strict");
const path = require("node:path");
const fs = require("node:fs");
const os = require("node:os");

const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "pty-run-test-"));
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");

const tmux = require("../lib/tmux");
const pty = require("../lib/pty-run");

describe("pty-run", () => {
  after(() => {
    try {
      fs.rmSync(TMP, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
    } catch {
      /* best-effort */
    }
  });

  beforeEach(() => {
    tmux.__reset();
  });

  it("spawnRun creates a new tmux session named ccam-lane-<id> when none exists", () => {
    const calls = [];
    tmux.__setExecImpl((args) => {
      calls.push(args);
      if (args[0] === "has-session") {
        const e = new Error("no such session");
        e.status = 1;
        throw e;
      }
      return "";
    });
    const handle = pty.spawnRun({ laneId: 42, cwd: "/tmp/repo", model: "opus" });
    assert.equal(handle.id, "ccam-lane-42");
    const newSessionCall = calls.find((c) => c[0] === "new-session");
    assert.ok(newSessionCall, "expected a new-session call");
    assert.deepEqual(newSessionCall.slice(0, 6), [
      "new-session",
      "-d",
      "-s",
      "ccam-lane-42",
      "-c",
      "/tmp/repo",
    ]);
    assert.ok(newSessionCall.includes("claude"));
    assert.ok(newSessionCall.includes("--model"));
    assert.ok(newSessionCall.includes("opus"));
  });

  it("spawnRun is a no-op (adopts) when the existing session's pane runs claude", () => {
    const calls = [];
    tmux.__setExecImpl((args) => {
      calls.push(args);
      if (args[0] === "display-message") return "claude\n";
      return ""; // has-session succeeds → already running
    });
    const handle = pty.spawnRun({ laneId: 7, cwd: "/tmp/repo" });
    assert.equal(handle.id, "ccam-lane-7");
    assert.ok(!calls.some((c) => c[0] === "new-session"), "must not create a duplicate session");
    assert.ok(!calls.some((c) => c[0] === "send-keys"), "must not type over a live agent");
  });

  it("spawnRun types the argv into an existing session idling at a shell prompt", () => {
    const calls = [];
    tmux.__setExecImpl((args) => {
      calls.push(args);
      if (args[0] === "display-message") return "bash\n";
      return ""; // has-session succeeds → session exists, pane is a shell
    });
    pty.spawnRun({ laneId: 8, cwd: "/tmp/repo", resumeSessionId: "abc12345" });
    assert.ok(!calls.some((c) => c[0] === "new-session"), "must not create a duplicate session");
    const literal = calls.find((c) => c[0] === "send-keys" && c[3] === "-l");
    assert.ok(literal, "expected a literal send-keys with the command line");
    assert.match(literal[4], /^'claude' .*'--resume' 'abc12345'$/);
    assert.ok(
      calls.some((c) => c[0] === "send-keys" && c[3] === "Enter"),
      "expected the command to be submitted"
    );
  });

  it("spawnRun single-quotes an initial prompt typed into an existing shell pane", () => {
    let literal = null;
    tmux.__setExecImpl((args) => {
      if (args[0] === "display-message") return "zsh\n";
      if (args[0] === "send-keys" && args[3] === "-l") literal = args[4];
      return "";
    });
    pty.spawnRun({ laneId: 9, cwd: "/tmp/repo", initialPrompt: "don't; rm -rf /" });
    assert.ok(literal.endsWith(`'don'\\''t; rm -rf /'`), literal);
  });

  it("spawnRun with resumeSessionId passes --resume in argv", () => {
    let newSessionArgv = null;
    tmux.__setExecImpl((args) => {
      if (args[0] === "has-session") {
        const e = new Error("gone");
        e.status = 1;
        throw e;
      }
      if (args[0] === "new-session") newSessionArgv = args;
      return "";
    });
    pty.spawnRun({ laneId: 1, cwd: "/tmp/repo", resumeSessionId: "abc12345" });
    assert.ok(newSessionArgv.includes("--resume"));
    assert.ok(newSessionArgv.includes("abc12345"));
  });

  it("spawnRun offers bypass in the pane's shift+tab cycle without starting in it", () => {
    let newSessionArgv = null;
    tmux.__setExecImpl((args) => {
      if (args[0] === "has-session") {
        const e = new Error("gone");
        e.status = 1;
        throw e;
      }
      if (args[0] === "new-session") newSessionArgv = args;
      return "";
    });
    pty.spawnRun({ laneId: 4, cwd: "/tmp/repo" });
    assert.ok(newSessionArgv.includes("--allow-dangerously-skip-permissions"));
    // ...as an OPTION only: the starting mode stays the requested one, and
    // `--dangerously-skip-permissions` (which would enable it) is never passed.
    assert.ok(!newSessionArgv.includes("--dangerously-skip-permissions"));
    const modeAt = newSessionArgv.indexOf("--permission-mode");
    assert.equal(newSessionArgv[modeAt + 1], "acceptEdits");
  });

  it("spawnRun appends a positional initial prompt after argv flags", () => {
    let newSessionArgv = null;
    tmux.__setExecImpl((args) => {
      if (args[0] === "has-session") {
        const e = new Error("gone");
        e.status = 1;
        throw e;
      }
      if (args[0] === "new-session") newSessionArgv = args;
      return "";
    });
    pty.spawnRun({ laneId: 3, cwd: "/tmp/repo", initialPrompt: "fix the bug" });
    assert.equal(newSessionArgv[newSessionArgv.length - 1], "fix the bug");
  });

  it("killRun calls tmux kill-session with the run id", () => {
    const calls = [];
    tmux.__setExecImpl((args) => {
      calls.push(args);
      return "";
    });
    assert.equal(pty.killRun("ccam-lane-5"), true);
    assert.ok(calls.some((c) => c[0] === "kill-session" && c[2] === "ccam-lane-5"));
  });

  it("listRuns reflects live tmux-session state, not cached memory", () => {
    tmux.__setExecImpl((args) => {
      if (args[0] === "list-sessions") return "ccam-lane-1\nccam-lane-2\n";
      return "";
    });
    const first = pty.listRuns();
    assert.deepEqual(first.map((r) => r.id).sort(), ["ccam-lane-1", "ccam-lane-2"]);

    // Session killed out-of-band (not through killRun) — next list() call
    // must self-correct, proving state is computed, not stored.
    tmux.__setExecImpl((args) => {
      if (args[0] === "list-sessions") return "ccam-lane-1\n";
      return "";
    });
    const second = pty.listRuns();
    assert.deepEqual(
      second.map((r) => r.id),
      ["ccam-lane-1"]
    );
  });

  it("laneIdFromRunId parses the numeric lane id back out", () => {
    assert.equal(pty.laneIdFromRunId("ccam-lane-42"), 42);
    assert.equal(pty.laneIdFromRunId("not-a-run-id"), null);
  });

  it("getRun returns the recorded prompt for a live run", () => {
    let sessionExists = false;
    tmux.__setExecImpl((args) => {
      if (args[0] === "has-session") {
        if (sessionExists) {
          return ""; // session exists
        }
        // Session doesn't exist yet
        const e = new Error("no such session");
        e.status = 1;
        throw e;
      }
      if (args[0] === "new-session") {
        sessionExists = true; // Mark session as created
      }
      return "";
    });
    const handle = pty.spawnRun({
      laneId: 99,
      cwd: "/tmp/test",
      initialPrompt: "the live prompt",
    });
    const retrieved = pty.getRun(handle.id);
    assert.equal(retrieved.id, "ccam-lane-99");
    assert.equal(retrieved.promptPreview, "the live prompt");
    assert.equal(retrieved.status, "running");
  });
});
