import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { chmodSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { test } from "node:test";
import { HardenError, hardenNode, validateHardenTarget } from "../../../lib/fleet/harden.mjs";

function sha256Hex(value) {
  return createHash("sha256").update(value).digest("hex");
}

function makeHardenItem(overrides = {}) {
  const content = overrides.content ?? "harden fixture content";
  const sourcePath = overrides.sourcePath ?? "fixtures/example.conf";
  const { content: _content, sourcePath: _sourcePath, ...rest } = overrides;
  return {
    id: "buildbox-root:test:etc/example.conf",
    source: { kind: "repo", path: sourcePath, ref: null },
    destination: "/etc/example.conf",
    ownership: "root",
    strategy: "copy",
    mode: "harden",
    content_digest: sha256Hex(content),
    owner: "fleet",
    content,
    ...rest,
  };
}

function createFakeTransport({ files = new Map(), bootId = "boot-before" } = {}) {
  let calls = 0;
  const snapshot = new Map();
  let currentBootId = bootId;

  const transport = {
    name: "ssh",
    get calls() {
      return calls;
    },
    async read(path) {
      calls += 1;
      if (!files.has(path)) {
        return { exists: false, digest: null, ownership: "root", mode: null, linkTarget: null };
      }
      const content = files.get(path);
      return {
        exists: true,
        digest: sha256Hex(content),
        ownership: "root",
        mode: "644",
        linkTarget: null,
      };
    },
    async write() {
      calls += 1;
      throw new Error("harden must not use transport.write for root items");
    },
    async unitState() {
      calls += 1;
      return { enabled: false, active: false };
    },
    async gitRef() {
      calls += 1;
      return null;
    },
  };

  const ops = {
    restored: false,
    async bootId() {
      calls += 1;
      return currentBootId;
    },
    async armDeadman() {
      calls += 1;
      for (const [path, content] of files) {
        snapshot.set(path, content);
      }
    },
    async disarmDeadman() {
      calls += 1;
    },
    async writeRoot(item, bytes) {
      calls += 1;
      snapshot.set(item.destination, files.get(item.destination));
      files.set(item.destination, bytes);
    },
    async reboot() {
      calls += 1;
      currentBootId = `${bootId}-after`;
    },
    async waitForReboot(before) {
      calls += 1;
      return currentBootId !== before ? currentBootId : null;
    },
    async restoreLastKnownGood() {
      calls += 1;
      ops.restored = true;
      for (const [path, content] of snapshot) {
        if (content === undefined) {
          files.delete(path);
        } else {
          files.set(path, content);
        }
      }
    },
  };

  return { transport, ops, files, get bootIdAfter() {
    return currentBootId;
  } };
}

function harness(options = {}) {
  const item = makeHardenItem(options.itemOverrides);
  const content = Buffer.from(item.content, "utf8");
  const repoRoot = mkdtempSync(join(tmpdir(), "fleet-harden-"));
  const sourceAbs = join(repoRoot, item.source.path);
  mkdirSync(dirname(sourceAbs), { recursive: true });
  writeFileSync(sourceAbs, content, { mode: 0o644 });
  const { transport, ops, files } = createFakeTransport();
  if (!options.startDrifted) {
    files.set(item.destination, content);
  }
  return { item, content, transport, ops, files, repoRoot };
}

test("rejects zero nodes before any transport call", async () => {
  const { transport } = createFakeTransport();
  await assert.rejects(
    () => hardenNode("", [makeHardenItem()], transport),
    (err) => err instanceof HardenError && /exactly one buildbox node/.test(err.message),
  );
  assert.equal(transport.calls, 0);
});

test("rejects multiple nodes before any transport call", async () => {
  const { transport } = createFakeTransport();
  await assert.rejects(
    () => hardenNode("debian1 debian2", [makeHardenItem()], transport),
    (err) => err instanceof HardenError && /multiple nodes/.test(err.message),
  );
  assert.equal(transport.calls, 0);
});

test("rejects implicit all-node selection before any transport call", async () => {
  const { transport } = createFakeTransport();
  await assert.rejects(
    () => hardenNode("*", [makeHardenItem()], transport),
    (err) => err instanceof HardenError && /implicit all-node/.test(err.message),
  );
  assert.equal(transport.calls, 0);
});

test("rejects workstation before any transport call", async () => {
  const { transport } = createFakeTransport();
  await assert.rejects(
    () => hardenNode("workstation", [makeHardenItem()], transport),
    (err) => err instanceof HardenError && /workstation/.test(err.message),
  );
  assert.equal(transport.calls, 0);
});

test("rejects non-ssh transport before any transport call", async () => {
  const transport = {
    name: "local",
    async read() {
      throw new Error("transport must not be called");
    },
    async write() {},
    async unitState() {
      return { enabled: false, active: false };
    },
    async gitRef() {
      return null;
    },
  };
  await assert.rejects(
    () => hardenNode("debian1", [makeHardenItem()], transport),
    (err) => err instanceof HardenError && /ssh transport/.test(err.message),
  );
});

test("validateHardenTarget mirrors hardenNode guards", () => {
  const ssh = { name: "ssh" };
  const local = { name: "local" };
  assert.throws(() => validateHardenTarget("", ssh), HardenError);
  assert.throws(() => validateHardenTarget("debian1,debian2", ssh), HardenError);
  assert.throws(() => validateHardenTarget("workstation", ssh), HardenError);
  assert.throws(() => validateHardenTarget("debian1", local), HardenError);
  assert.doesNotThrow(() => validateHardenTarget("debian1", ssh));
});

test("applies root items, observes boot-id change, and returns a clean finalAudit", async () => {
  const { item, content, transport, ops, repoRoot } = harness({ startDrifted: true });
  const report = await hardenNode("debian1", [item], transport, {
    deps: { hardenOps: ops },
    repoRoot,
  });

  assert.equal(report.node, "debian1");
  assert.deepEqual(report.changed, [item.id]);
  assert.equal(report.rebooted, true);
  assert.equal(report.bootIdBefore, "boot-before");
  assert.equal(report.bootIdAfter, "boot-before-after");
  assert.notEqual(report.bootIdBefore, report.bootIdAfter);
  assert.equal(report.finalAudit.ok, true);
  assert.equal(report.finalAudit.drift.length, 0);
  assert.equal(sha256Hex(content), sha256Hex(content));
});

test("failed post-reboot audit restores last-known-good and returns a failure", async () => {
  const { item, transport, ops, files, repoRoot } = harness({ startDrifted: true });
  const original = ops.writeRoot;
  ops.writeRoot = async (driftItem, bytes) => {
    await original(driftItem, bytes);
    files.set(driftItem.destination, Buffer.from("wrong-after-reboot", "utf8"));
  };

  await assert.rejects(
    () => hardenNode("debian1", [item], transport, { deps: { hardenOps: ops }, repoRoot }),
    (err) => {
      assert.ok(err instanceof HardenError);
      assert.match(err.message, /post-reboot audit failed/);
      assert.ok(err.report);
      assert.equal(err.report.rebooted, true);
      assert.equal(err.report.finalAudit.ok, false);
      return true;
    },
  );
  assert.equal(ops.restored, true);
});

test("ignores non-harden items", async () => {
  const { item, transport, ops, files, repoRoot } = harness({ startDrifted: true });
  const convergeItem = {
    ...makeHardenItem({
      id: "user:config",
      destination: "/home/user/.config/example",
      ownership: "user",
      mode: "converge",
    }),
    content: "user content",
  };
  files.set(convergeItem.destination, Buffer.from("user content", "utf8"));

  const report = await hardenNode("debian1", [convergeItem, item], transport, {
    deps: { hardenOps: ops },
    repoRoot,
  });

  assert.deepEqual(report.changed, [item.id]);
  assert.equal(report.finalAudit.ok, true);
});

function defaultOpsHarness({ sourceMode }) {
  const { item, content, transport, files, repoRoot } = harness({ startDrifted: true });
  chmodSync(join(repoRoot, item.source.path), sourceMode);
  const commands = [];
  let bootId = "boot-before";

  const sshExec = async (argv) => {
    if (argv[0] === "scp") {
      return { stdout: "", stderr: "", code: 0 };
    }
    const remote = argv[argv.length - 1];
    commands.push(remote);
    if (remote.includes("/proc/sys/kernel/random/boot_id")) {
      return { stdout: `${bootId}\n`, stderr: "", code: 0 };
    }
    if (remote.includes("systemctl reboot")) {
      bootId = "boot-after";
      return { stdout: "", stderr: "", code: 0 };
    }
    if (remote.includes("install -D -m")) {
      files.set(item.destination, content);
    }
    return { stdout: "", stderr: "", code: 0 };
  };

  return {
    item,
    transport,
    commands,
    options: {
      repoRoot,
      sshExec,
      deps: {
        loadRegistry: () => ({
          hosts: [
            {
              name: "debian1",
              state: "reachable",
              ssh_alias: "debian1",
              access: {
                tailscale_ip: {
                  target: "user@100.64.0.1",
                  port: 2222,
                  identity_file: "~/.ssh/id_ed25519_buildbox",
                },
                lan: null,
                tailscale_ssh: null,
              },
            },
          ],
        }),
      },
    },
  };
}

function installCommand(commands) {
  return commands.find((cmd) => cmd.includes("install -D -m") && cmd.includes("harden-staging"));
}

test("root writes install a locally computed canonical mode, never the staging file's stat", async () => {
  const plain = defaultOpsHarness({ sourceMode: 0o664 });
  const plainReport = await hardenNode("debian1", [plain.item], plain.transport, plain.options);
  assert.equal(plainReport.finalAudit.ok, true);
  const plainCmd = installCommand(plain.commands);
  assert.ok(plainCmd, "expected a root install command");
  assert.ok(!plainCmd.includes("stat -c"), `install command still stats the staging file: ${plainCmd}`);
  assert.match(plainCmd, /mode='644'/);
  assert.match(plainCmd, /install -D -m "\$mode"/);

  const exec = defaultOpsHarness({ sourceMode: 0o664 | 0o100 });
  await hardenNode("debian1", [exec.item], exec.transport, exec.options).catch(() => {});
  assert.match(installCommand(exec.commands), /mode='755'/);
});
