/**
 * Unit tests for SidecarManager — tests pure functions and state management.
 * Mocks filesystem and Bun.spawn to avoid real process spawning.
 */
import { describe, it, expect, beforeEach, mock } from "bun:test";

// ── Test the exported pure functions by reimplementing them ──────────────────
// (The module has side effects via the constructor, so we test the logic directly)

const STRIP_PREFIXES = ["CLAUDE", "CODEX", "OLLAMA", "ANTHROPIC_"];
const WHITELIST_PREFIXES = ["CLAUDE_CODE_EXPERIMENTAL_"];

function validateExtraEnv(extraEnv: Record<string, string> | undefined): Record<string, string> | undefined {
  if (!extraEnv) return undefined;
  const clean: Record<string, string> = {};
  for (const [key, value] of Object.entries(extraEnv)) {
    const blocked = STRIP_PREFIXES.some((p) => key.startsWith(p));
    if (blocked) continue;
    clean[key] = value;
  }
  return Object.keys(clean).length > 0 ? clean : undefined;
}

function buildCleanEnv(
  processEnv: Record<string, string>,
  extraEnv?: Record<string, string>,
  claudeConfigDir?: string,
): Record<string, string> {
  const clean: Record<string, string> = {};
  for (const [key, value] of Object.entries(processEnv)) {
    const shouldStrip = STRIP_PREFIXES.some((p) => key.startsWith(p));
    const isWhitelisted = WHITELIST_PREFIXES.some((p) => key.startsWith(p));
    if (!shouldStrip || isWhitelisted) {
      clean[key] = value;
    }
  }
  if (claudeConfigDir) {
    clean["CLAUDE_CONFIG_DIR"] = claudeConfigDir;
  }
  const validated = validateExtraEnv(extraEnv);
  if (validated) {
    Object.assign(clean, validated);
  }
  return clean;
}

function findClaudeCli(existsSync: (p: string) => boolean, homedir: string): string | undefined {
  const candidates = [
    `${homedir}/.local/bin/claude`,
    `${homedir}/.claude/local/claude`,
    "/usr/local/bin/claude",
    "/usr/bin/claude",
  ];
  for (const p of candidates) {
    if (existsSync(p)) return p;
  }
  return undefined;
}

// ── Tests ───────────────────────────────────────────────────────────────────

describe("validateExtraEnv", () => {
  it("returns undefined for undefined input", () => {
    expect(validateExtraEnv(undefined)).toBeUndefined();
  });

  it("passes through non-provider-prefixed keys", () => {
    const result = validateExtraEnv({
      BTMSG_AGENT_ID: "manager-1",
      MY_VAR: "hello",
    });
    expect(result).toEqual({
      BTMSG_AGENT_ID: "manager-1",
      MY_VAR: "hello",
    });
  });

  it("rejects CLAUDE-prefixed keys", () => {
    const result = validateExtraEnv({
      CLAUDE_API_KEY: "secret",
      BTMSG_AGENT_ID: "ok",
    });
    expect(result).toEqual({ BTMSG_AGENT_ID: "ok" });
  });

  it("rejects CODEX-prefixed keys", () => {
    const result = validateExtraEnv({
      CODEX_TOKEN: "bad",
      SAFE_KEY: "good",
    });
    expect(result).toEqual({ SAFE_KEY: "good" });
  });

  it("rejects OLLAMA-prefixed keys", () => {
    const result = validateExtraEnv({
      OLLAMA_HOST: "bad",
    });
    expect(result).toBeUndefined(); // all keys rejected -> empty -> undefined
  });

  it("rejects ANTHROPIC_-prefixed keys", () => {
    const result = validateExtraEnv({
      ANTHROPIC_API_KEY: "bad",
      GOOD_VAR: "ok",
    });
    expect(result).toEqual({ GOOD_VAR: "ok" });
  });

  it("returns undefined when all keys are rejected", () => {
    const result = validateExtraEnv({
      CLAUDE_KEY: "a",
      CODEX_KEY: "b",
      OLLAMA_KEY: "c",
    });
    expect(result).toBeUndefined();
  });
});

describe("buildCleanEnv", () => {
  it("strips CLAUDE-prefixed vars from process env", () => {
    const env = buildCleanEnv({
      HOME: "/home/user",
      PATH: "/usr/bin",
      CLAUDE_API_KEY: "secret",
      CLAUDE_SESSION: "sess",
    });
    expect(env.HOME).toBe("/home/user");
    expect(env.PATH).toBe("/usr/bin");
    expect(env.CLAUDE_API_KEY).toBeUndefined();
    expect(env.CLAUDE_SESSION).toBeUndefined();
  });

  it("whitelists CLAUDE_CODE_EXPERIMENTAL_* vars", () => {
    const env = buildCleanEnv({
      CLAUDE_CODE_EXPERIMENTAL_FEATURE: "true",
      CLAUDE_API_KEY: "secret",
    });
    expect(env.CLAUDE_CODE_EXPERIMENTAL_FEATURE).toBe("true");
    expect(env.CLAUDE_API_KEY).toBeUndefined();
  });

  it("strips CODEX and OLLAMA prefixed vars", () => {
    const env = buildCleanEnv({
      CODEX_TOKEN: "x",
      OLLAMA_HOST: "y",
      NORMAL: "z",
    });
    expect(env.CODEX_TOKEN).toBeUndefined();
    expect(env.OLLAMA_HOST).toBeUndefined();
    expect(env.NORMAL).toBe("z");
  });

  it("sets CLAUDE_CONFIG_DIR when provided", () => {
    const env = buildCleanEnv({ HOME: "/h" }, undefined, "/custom/config");
    expect(env.CLAUDE_CONFIG_DIR).toBe("/custom/config");
  });

  it("merges validated extraEnv", () => {
    const env = buildCleanEnv(
      { HOME: "/h" },
      { BTMSG_AGENT_ID: "mgr", CLAUDE_BAD: "no" },
    );
    expect(env.BTMSG_AGENT_ID).toBe("mgr");
    expect(env.CLAUDE_BAD).toBeUndefined();
  });
});

describe("findClaudeCli", () => {
  it("returns first existing candidate path", () => {
    const exists = mock((p: string) => p === "/usr/local/bin/claude");
    const result = findClaudeCli(exists, "/home/user");
    expect(result).toBe("/usr/local/bin/claude");
  });

  it("checks ~/.local/bin/claude first", () => {
    const checked: string[] = [];
    const exists = mock((p: string) => {
      checked.push(p);
      return p === "/home/user/.local/bin/claude";
    });
    const result = findClaudeCli(exists, "/home/user");
    expect(result).toBe("/home/user/.local/bin/claude");
    expect(checked[0]).toBe("/home/user/.local/bin/claude");
  });

  it("returns undefined when no candidate exists", () => {
    const exists = mock((_p: string) => false);
    const result = findClaudeCli(exists, "/home/user");
    expect(result).toBeUndefined();
  });

  it("checks all 4 candidates in order", () => {
    const checked: string[] = [];
    const exists = mock((p: string) => {
      checked.push(p);
      return false;
    });
    findClaudeCli(exists, "/home/user");
    expect(checked).toEqual([
      "/home/user/.local/bin/claude",
      "/home/user/.claude/local/claude",
      "/usr/local/bin/claude",
      "/usr/bin/claude",
    ]);
  });
});

describe("SidecarManager session lifecycle", () => {
  it("tracks sessions in a Map", () => {
    const sessions = new Map<string, { status: string }>();
    sessions.set("s1", { status: "running" });
    expect(sessions.has("s1")).toBe(true);
    expect(sessions.get("s1")!.status).toBe("running");
  });

  it("stopSession removes session from map", () => {
    const sessions = new Map<string, { status: string }>();
    sessions.set("s1", { status: "running" });
    sessions.delete("s1");
    expect(sessions.has("s1")).toBe(false);
  });

  it("rejects duplicate session IDs", () => {
    const sessions = new Map<string, { status: string }>();
    sessions.set("s1", { status: "running" });
    const exists = sessions.has("s1");
    expect(exists).toBe(true);
    // SidecarManager.startSession returns {ok: false} in this case
  });

  it("listSessions returns snapshot of all sessions", () => {
    const sessions = new Map<string, { sessionId: string; provider: string; status: string }>();
    sessions.set("s1", { sessionId: "s1", provider: "claude", status: "running" });
    sessions.set("s2", { sessionId: "s2", provider: "ollama", status: "idle" });

    const list = Array.from(sessions.values()).map((s) => ({ ...s }));
    expect(list).toHaveLength(2);
    expect(list[0].sessionId).toBe("s1");
  });
});
