import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { loadRegistry } from "../../workstation/claude/lib/buildbox-registry.mjs";
import { expandNode, rejectDuplicateDestinations } from "../../../lib/fleet/expand.mjs";
import { FleetError, loadFleet } from "../../../lib/fleet/loader.mjs";

const SHIPPED_REGISTRY = join(
  dirname(fileURLToPath(import.meta.url)),
  "../../workstation/claude/buildbox-hosts.json",
);
const DEVTOOLS_PATH = join(
  dirname(fileURLToPath(import.meta.url)),
  "../../buildbox/devtools.json",
);

const registryLoader = () => loadRegistry({ BUILDBOX_HOSTS_CONFIG: SHIPPED_REGISTRY });

const fleet = () => loadFleet(undefined, { loadRegistry: registryLoader });

const snapshotKey = (items) =>
  JSON.stringify(
    items.map((item) => ({
      id: item.id,
      source: item.source,
      destination: item.destination,
      ownership: item.ownership,
      strategy: item.strategy,
      mode: item.mode,
      content_digest: item.content_digest,
      owner: item.owner,
    })),
  );

test("four-node snapshot expansion is deterministic", () => {
  const f = fleet();
  const workstation = expandNode(f, "workstation");
  const debian1 = expandNode(f, "debian1");
  const debian2 = expandNode(f, "debian2");
  const debian3 = expandNode(f, "debian3");

  assert.ok(workstation.length > 0);
  assert.ok(debian1.length > 0);
  assert.equal(debian1.length, debian2.length);
  assert.equal(debian2.length, debian3.length);

  const again = expandNode(f, "workstation");
  assert.equal(snapshotKey(workstation), snapshotKey(again));
});

test("debian builders differ only by node identity and host_ref in the declaration, not expansion", () => {
  const f = fleet();
  const debian1 = expandNode(f, "debian1");
  const debian2 = expandNode(f, "debian2");
  const debian3 = expandNode(f, "debian3");

  assert.equal(snapshotKey(debian1), snapshotKey(debian2));
  assert.equal(snapshotKey(debian2), snapshotKey(debian3));
});

test("no declared node routes a root-owned item through converge", () => {
  const f = fleet();
  for (const name of f.nodeNames()) {
    const items = expandNode(f, name);
    const converge = items.filter((item) => item.mode === "converge");
    assert.deepEqual(
      converge.filter((item) => item.ownership === "root"),
      [],
      `node ${name} routes a root-owned item through converge`,
    );
    const convergeDestinations = new Set(converge.map((item) => item.destination));
    assert.deepEqual(
      items.filter(
        (item) => item.mode === "harden" && convergeDestinations.has(item.destination),
      ),
      [],
      `node ${name} has a destination claimed by both converge and harden`,
    );
  }
});

test("workstation emits no harden items", () => {
  const harden = expandNode(fleet(), "workstation").filter((item) => item.mode === "harden");
  assert.deepEqual(harden, []);
});

test("every debian builder declares buildbox-root and expands harden items", () => {
  const f = fleet();
  for (const name of ["debian1", "debian2", "debian3"]) {
    assert.ok(
      f.node(name).profiles.includes("buildbox-root"),
      `node ${name} does not declare buildbox-root, so harden would select nothing`,
    );
    assert.ok(expandNode(f, name).some((item) => item.mode === "harden"));
  }
});

test("buildbox-root profile emits harden-only root items", () => {
  const items = expandNode(
    {
      node() {
        return { profiles: ["buildbox-root"] };
      },
    },
    "fixture",
  );
  assert.ok(items.length > 0);
  for (const item of items) {
    assert.equal(item.ownership, "root");
    assert.equal(item.mode, "harden");
    assert.equal(item.owner, "fleet");
  }
});

test("duplicate destination throws before any caller can contact a node", () => {
  const items = [
    {
      id: "a",
      destination: "~/.claude/hooks",
      source: { kind: "repo", path: "modules/workstation/claude/hooks", ref: null },
      ownership: "user",
      strategy: "symlink",
      mode: "converge",
      content_digest: "abc",
      owner: "fleet",
    },
    {
      id: "b",
      destination: "~/.claude/hooks",
      source: { kind: "repo", path: "modules/workstation/claude/lib", ref: null },
      ownership: "user",
      strategy: "symlink",
      mode: "converge",
      content_digest: "def",
      owner: "fleet",
    },
  ];
  assert.throws(() => rejectDuplicateDestinations(items), /duplicate destination/);
});

test("unknown profile throws", () => {
  assert.throws(
    () =>
      expandNode(
        {
          node() {
            return { profiles: ["shared-agent-tools", "not-a-real-expander-profile"] };
          },
        },
        "fixture",
      ),
    (err) => err instanceof FleetError && /unknown profile/.test(err.message),
  );
});

test("workstation owns agent homes while buildboxes retain fleet-owned devtools", () => {
  const workstation = expandNode(fleet(), "workstation");
  const buildbox = expandNode(fleet(), "debian1");
  const workstationAgentHome = workstation.filter((item) => item.id.startsWith("agent-home:"));
  const buildboxAgentHome = buildbox.filter((item) => item.id.startsWith("agent-home:"));
  const devtools = buildbox.filter((item) => item.id.startsWith("devtools:"));
  assert.ok(workstationAgentHome.length > 0);
  assert.deepEqual(buildboxAgentHome, []);
  assert.ok(devtools.length > 0);
  for (const item of workstationAgentHome) {
    assert.equal(item.owner, "remote-seat-provision");
  }
  for (const item of devtools) {
    assert.equal(item.owner, "fleet");
  }
});

test("deploy-sourced manifest entries use deploy kind without restating refs inline", () => {
  const items = expandNode(fleet(), "workstation");
  const deployItems = items.filter((item) => item.source.kind === "deploy");
  assert.ok(deployItems.length > 0);
  for (const item of deployItems) {
    assert.equal(item.source.ref, null);
    assert.match(item.source.path, /^modules\/workstation\//);
  }
});

test("pinned devtools refs come from devtools.json only", () => {
  const spec = JSON.parse(readFileSync(DEVTOOLS_PATH, "utf8"));
  const items = expandNode(fleet(), "debian1");
  for (const tool of spec.tools) {
    const item = items.find((entry) => entry.id === `devtools:tool:${tool.name}`);
    assert.ok(item, `missing devtools tool item for ${tool.name}`);
    assert.equal(item.source.kind, "ref");
    assert.equal(item.source.path, tool.repo);
    assert.equal(item.source.ref, tool.ref);
  }
  for (const vendor of spec.vendor) {
    const item = items.find((entry) => entry.id === `devtools:vendor:${vendor.name}`);
    assert.ok(item, `missing devtools vendor item for ${vendor.name}`);
    assert.equal(item.source.kind, "ref");
    assert.equal(item.source.ref, vendor.version);
  }
});
