/**
 * Integration tests for `penguin config vault set|list|remove` (run through commander's
 * parseAsync for the full command path, with PENGUIN_HOME pointed at a temp directory):
 * writes to a hidden .vault.toml (mode 0600), list masks values without leaking
 * plaintext, remove raises an error on a missing key, --agent-id targets a specific
 * Agent, and an invalid key name / an overlong value exit with a non-zero code.
 */
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Command } from "commander";
import { agentVaultPath, DEFAULT_PROJECT_ID } from "@prismshadow/penguin-core";
import { registerConfigCommand } from "../src/commands/config.js";
import { getMessages } from "../src/i18n.js";

let tmpRoot: string;
let prevHome: string | undefined;

beforeEach(async () => {
  prevHome = process.env.PENGUIN_HOME;
  tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "penguin-cli-vault-"));
  process.env.PENGUIN_HOME = tmpRoot;
});

afterEach(async () => {
  if (prevHome === undefined) delete process.env.PENGUIN_HOME;
  else process.env.PENGUIN_HOME = prevHome;
  await fs.rm(tmpRoot, { recursive: true, force: true });
});

/** Runs a `penguin config vault …` command, capturing stdout/stderr and the exit code (without actually exiting the process). */
async function runVault(args: string[]): Promise<{ out: string; err: string; code: number }> {
  const program = new Command();
  program.exitOverride();
  registerConfigCommand(program, getMessages("en"));
  const out: string[] = [];
  const err: string[] = [];
  const outSpy = vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
    out.push(String(chunk));
    return true;
  });
  const errSpy = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
    err.push(String(chunk));
    return true;
  });
  const prevExitCode = process.exitCode;
  process.exitCode = undefined;
  try {
    await program.parseAsync(["node", "penguin", "config", "vault", ...args]);
    return { out: out.join(""), err: err.join(""), code: Number(process.exitCode ?? 0) };
  } finally {
    outSpy.mockRestore();
    errSpy.mockRestore();
    process.exitCode = prevExitCode;
  }
}

describe("penguin config vault", () => {
  it("set -> list (masked) -> remove full round trip; stored as hidden .vault.toml with 0600", async () => {
    const set = await runVault(["set", "--key", "MY_KEY", "--value", "vault-secret-9876"]);
    expect(set.code).toBe(0);
    expect(set.out).toContain("Saved vault entry MY_KEY.");

    const file = agentVaultPath(tmpRoot, DEFAULT_PROJECT_ID, "default_agent");
    expect(path.basename(file)).toBe(".vault.toml");
    // POSIX-only: Windows has no owner-only mode bits (chmod maps to the read-only attribute).
    if (process.platform !== "win32") {
      expect((await fs.stat(file)).mode & 0o777).toBe(0o600);
    }
    expect(await fs.readFile(file, "utf8")).toContain("vault-secret-9876");

    const list = await runVault(["list"]);
    expect(list.code).toBe(0);
    expect(list.out).toContain("MY_KEY");
    expect(list.out).toContain("****9876");
    // Plaintext never appears in list output.
    expect(list.out).not.toContain("vault-secret-9876");

    const removed = await runVault(["remove", "--key", "MY_KEY"]);
    expect(removed.code).toBe(0);
    expect(removed.out).toContain("Removed vault entry MY_KEY.");
    const empty = await runVault(["list"]);
    expect(empty.out).toContain("The vault is empty.");
  });

  it("--agent-id targets that Agent's vault without touching default_agent", async () => {
    const set = await runVault([
      "set",
      "--key",
      "ONLY_A",
      "--value",
      "va-secret-value-1",
      "--agent-id",
      "agent-a",
    ]);
    expect(set.code).toBe(0);
    expect(
      await fs.readFile(agentVaultPath(tmpRoot, DEFAULT_PROJECT_ID, "agent-a"), "utf8"),
    ).toContain("ONLY_A");
    const defaultList = await runVault(["list"]);
    expect(defaultList.out).toContain("The vault is empty.");
  });

  it("--root picks the data root (beats PENGUIN_HOME); set/list both target that root", async () => {
    const otherRoot = await fs.mkdtemp(path.join(os.tmpdir(), "penguin-cli-vault-root-"));
    try {
      const set = await runVault([
        "set",
        "--key",
        "ROOTED_KEY",
        "--value",
        "root-secret-value-1",
        "--root",
        otherRoot,
      ]);
      expect(set.code).toBe(0);
      expect(
        await fs.readFile(agentVaultPath(otherRoot, DEFAULT_PROJECT_ID, "default_agent"), "utf8"),
      ).toContain("ROOTED_KEY");
      // The root directory pointed to by PENGUIN_HOME is unaffected.
      const defaultList = await runVault(["list"]);
      expect(defaultList.out).toContain("The vault is empty.");
      const rootedList = await runVault(["list", "--root", otherRoot]);
      expect(rootedList.out).toContain("ROOTED_KEY");
    } finally {
      await fs.rm(otherRoot, { recursive: true, force: true });
    }
  });

  it("invalid key name / oversized value exit nonzero and print the reason; removing a missing key errors", async () => {
    const badKey = await runVault(["set", "--key", "1BAD", "--value", "v"]);
    expect(badKey.code).toBe(1);
    expect(badKey.err).toContain("Invalid vault key");

    const tooLong = await runVault(["set", "--key", "OK_BIG", "--value", "x".repeat(8193)]);
    expect(tooLong.code).toBe(1);
    expect(tooLong.err).toContain("too long");

    const ghost = await runVault(["remove", "--key", "GHOST"]);
    expect(ghost.code).toBe(1);
    expect(ghost.err).toContain("Vault entry GHOST does not exist.");
  });
});
