import assert from "node:assert/strict";
import { randomBytes } from "node:crypto";
import { chmodSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { mkdtempSync } from "node:fs";
import type { PiSshConfig, VaultPaths } from "../src/config.ts";
import { decryptConfig, encryptConfig, loadVault, rotateVaultKey, saveVault } from "../src/vault.ts";

function fixture(): PiSshConfig {
  return {
    version: 1,
    hosts: {
      packaging: {
        label: "Packaging",
        hostName: "192.0.2.10",
        user: "builder",
        port: 22,
        auth: { type: "password", password: "server-secret", method: "auto" },
        hostKey: { algorithm: "ssh-ed25519", fingerprint: "SHA256:fixture" },
      },
    },
  };
}

function paths(): VaultPaths {
  const directory = mkdtempSync(join(tmpdir(), "pi-ssh-vault-"));
  return {
    directory,
    encryptedConfig: join(directory, "hosts.enc"),
    key: join(directory, "vault.key"),
  };
}

test("encrypts and authenticates the complete configuration", () => {
  const key = randomBytes(32);
  const encrypted = encryptConfig(fixture(), key);
  assert.doesNotMatch(encrypted, /server-secret|192\.0\.2\.10|builder/);
  assert.deepEqual(decryptConfig(encrypted, key), fixture());

  const envelope = JSON.parse(encrypted);
  envelope.ciphertext = `${envelope.ciphertext.slice(0, -2)}AA`;
  assert.throws(() => decryptConfig(JSON.stringify(envelope), key), /integrity verification/);
  assert.throws(() => decryptConfig(encrypted, randomBytes(32)), /integrity verification/);
});

test("writes an owner-only key and encrypted config without plaintext", () => {
  const target = paths();
  saveVault(fixture(), target);
  assert.deepEqual(loadVault(target), fixture());
  assert.doesNotMatch(readFileSync(target.encryptedConfig, "utf8"), /server-secret/);
  assert.equal(Buffer.from(readFileSync(target.key, "utf8").trim(), "base64").length, 32);
  if (process.platform !== "win32") {
    assert.equal(statSync(target.directory).mode & 0o777, 0o700);
    assert.equal(statSync(target.encryptedConfig).mode & 0o777, 0o600);
    assert.equal(statSync(target.key).mode & 0o777, 0o600);
  }
});

test("fails closed when vault files are missing or too broadly readable", () => {
  const target = paths();
  assert.throws(() => loadVault(target), /not configured/);
  mkdirSync(target.directory, { recursive: true });
  writeFileSync(target.key, `${randomBytes(32).toString("base64")}\n`, { mode: 0o600 });
  assert.throws(() => loadVault(target), /not configured/);
  saveVault(fixture(), target);
  if (process.platform !== "win32") {
    chmodSync(target.encryptedConfig, 0o644);
    assert.throws(() => loadVault(target), /permissions/);
  }
});

test("rotates the adjacent key while preserving the encrypted configuration", () => {
  const target = paths();
  saveVault(fixture(), target);
  const oldKey = readFileSync(target.key, "utf8");
  rotateVaultKey(target);
  assert.notEqual(readFileSync(target.key, "utf8"), oldKey);
  assert.deepEqual(loadVault(target), fixture());
});
