#!/usr/bin/env bash
# The fleet declaration is the only host list: build-remote.json keeps per-node tuning,
# a leftover `hosts` key fails the audit, and every consumer resolves hosts through the loader.
set -uo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
DECKCTL="$ROOT/bin/deckctl"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

pass=0
fail=0
ok() { pass=$((pass + 1)); printf 'PASS %s\n' "$1"; }
bad() { fail=$((fail + 1)); printf 'FAIL %s\n' "$1"; }

REGISTRY="$TMP/buildbox-hosts.json"
cat >"$REGISTRY" <<'JSON'
{
  "schema_version": 1,
  "hosts": [
    { "name": "debian1", "ssh_alias": "debian1", "state": "reachable",
      "machine_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "roles": ["builder"],
      "access": { "lan": null,
        "tailscale_ip": { "host": "100.64.0.1", "port": 2222, "user": "user", "identity_file": null },
        "tailscale_ssh": null },
      "rustdesk": null, "notes": "debian1 fixture" },
    { "name": "debian2", "ssh_alias": "debian2", "state": "reachable",
      "machine_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "roles": ["builder"],
      "access": { "lan": null,
        "tailscale_ip": { "host": "100.64.0.2", "port": 2222, "user": "user", "identity_file": null },
        "tailscale_ssh": null },
      "rustdesk": null, "notes": "debian2 fixture" }
  ],
  "orders": { "build": ["debian1", "debian2"] }
}
JSON

FLEET="$TMP/fleet.json"
cat >"$FLEET" <<'JSON'
{
  "schema_version": 1,
  "nodes": {
    "workstation": { "transport": "local", "roles": ["control", "agent-runtime"],
      "profiles": ["shared-agent-tools", "workstation"], "execution": "last-resort" },
    "debian1": { "transport": "ssh", "host_ref": "debian1", "roles": ["builder", "agent-runtime"],
      "profiles": ["shared-agent-tools", "buildbox"], "execution": "normal" }
  },
  "fallback": { "enabled": false, "node": "workstation", "requires_all_unavailable": ["debian1"],
    "max_concurrent_local_jobs": 1, "activation_windows": 3, "health_window_sec": 60, "lease_ttl_sec": 900 }
}
JSON

export DECKCTL_FLEET_FILE="$FLEET"
export BUILDBOX_HOSTS_CONFIG="$REGISTRY"
unset OVERDECK_SEAT_HOST

write_tuning() { # $1 = path, $2 = json body
  printf '%s\n' "$2" >"$1"
}

# --- a leftover `hosts` key fails the audit, naming the key
write_tuning "$TMP/with-hosts.json" '{ "enabled": true, "hosts": ["debian1"], "port": 2222 }'
BUILD_REMOTE_CONFIG="$TMP/with-hosts.json" "$DECKCTL" fleet audit >"$TMP/hosts.out" 2>"$TMP/hosts.err"
rc=$?
if [[ $rc -ne 0 ]] && grep -q 'hosts: leftover host list' "$TMP/hosts.err"; then
  ok 'leftover hosts key fails the audit naming the key'
else
  bad "leftover hosts key fails the audit naming the key (rc=$rc stderr=$(tr '\n' ' ' <"$TMP/hosts.err"))"
fi

# --- a leftover legacy `host` key fails the audit, naming the key
write_tuning "$TMP/with-host.json" '{ "enabled": true, "host": "debian1", "port": 2222 }'
BUILD_REMOTE_CONFIG="$TMP/with-host.json" "$DECKCTL" fleet audit >"$TMP/host.out" 2>"$TMP/host.err"
rc=$?
if [[ $rc -ne 0 ]] && grep -q 'host: leftover legacy host' "$TMP/host.err"; then
  ok 'leftover legacy host key fails the audit naming the key'
else
  bad "leftover legacy host key fails the audit naming the key (rc=$rc stderr=$(tr '\n' ' ' <"$TMP/host.err"))"
fi

# --- per-node tuning for an undeclared node fails closed
write_tuning "$TMP/bad-node.json" '{ "enabled": true, "debian9": { "port": 2222 } }'
BUILD_REMOTE_CONFIG="$TMP/bad-node.json" "$DECKCTL" fleet audit >"$TMP/badnode.out" 2>"$TMP/badnode.err"
rc=$?
if [[ $rc -ne 0 ]] && grep -q 'debian9: per-node tuning' "$TMP/badnode.err"; then
  ok 'tuning for an undeclared node fails closed naming it'
else
  bad "tuning for an undeclared node fails closed naming it (rc=$rc stderr=$(tr '\n' ' ' <"$TMP/badnode.err"))"
fi

# --- per-node tuning for a declared node loads, and local_fallback grants nothing
write_tuning "$TMP/good.json" '{ "enabled": true, "local_fallback": true, "port": 2222, "debian1": { "port": 2223 } }'
BUILD_REMOTE_CONFIG="$TMP/good.json" node --input-type=module -e '
import assert from "node:assert/strict";
import { loadFleet } from "'"$ROOT"'/lib/fleet/loader.mjs";
const fleet = loadFleet();
assert.deepEqual(fleet.nodeNames(), ["workstation", "debian1"]);
assert.equal(fleet.fallback.enabled, false);
assert.equal(Object.hasOwn(fleet.fallback, "local_fallback"), false);
' >"$TMP/good.out" 2>"$TMP/good.err"
if [[ $? -eq 0 ]]; then
  ok 'declared-node tuning loads and local_fallback grants nothing'
else
  bad "declared-node tuning loads and local_fallback grants nothing ($(tr '\n' ' ' <"$TMP/good.err"))"
fi

# --- remote-build resolves membership from the fleet, order and state from the registry
BUILD_REMOTE_CONFIG="$TMP/good.json" node --input-type=module -e '
import assert from "node:assert/strict";
import { registryBuildHosts } from "'"$ROOT"'/modules/workstation/claude/lib/remote-build.mjs";
assert.deepEqual(registryBuildHosts(), ["debian1"]);
' >"$TMP/rb.out" 2>"$TMP/rb.err"
if [[ $? -eq 0 ]]; then
  ok 'registryBuildHosts returns only registry hosts the fleet declares as builders'
else
  bad "registryBuildHosts returns only registry hosts the fleet declares as builders ($(tr '\n' ' ' <"$TMP/rb.err"))"
fi

# --- no declared builder in the registry order raises instead of reporting an empty list
NO_BUILDER="$TMP/fleet-no-builder.json"
python3 - "$FLEET" "$NO_BUILDER" <<'PY'
import json, sys
doc = json.load(open(sys.argv[1]))
doc["nodes"]["debian1"]["roles"] = ["agent-runtime"]
doc["fallback"]["requires_all_unavailable"] = []
json.dump(doc, open(sys.argv[2], "w"))
PY
BUILD_REMOTE_CONFIG="$TMP/good.json" DECKCTL_FLEET_FILE="$NO_BUILDER" node --input-type=module -e '
import assert from "node:assert/strict";
import { registryBuildHosts } from "'"$ROOT"'/modules/workstation/claude/lib/remote-build.mjs";
assert.throws(() => registryBuildHosts(), /refusing to run locally/);
' >"$TMP/nb.out" 2>"$TMP/nb.err"
if [[ $? -eq 0 ]]; then
  ok 'an empty fleet-declared build set raises instead of degrading to local'
else
  bad "an empty fleet-declared build set raises instead of degrading to local ($(tr '\n' ' ' <"$TMP/nb.err"))"
fi

# --- deckctl agents takes its remote host list from the loader, not from build-remote.json
hosts_out=$(cd "$ROOT" && DECKCTL_ROOT="$ROOT" BUILD_REMOTE_CONFIG="$TMP/good.json" bash -c '
source lib/deckctl/agents.sh
agents_fleet_hosts')
if [[ "$hosts_out" == "debian1" ]]; then
  ok 'agents resolves remote hosts through the fleet loader'
else
  bad "agents resolves remote hosts through the fleet loader (got '$hosts_out')"
fi

if grep -q '"hosts"' "$ROOT/lib/deckctl/agents.sh"; then
  bad 'agents.sh still parses a hosts key'
else
  ok 'agents.sh no longer parses a hosts key'
fi

# --- loadFleet reads the declaration only: a poisoned build-remote.json under HOME is
# --- invisible to it, while the exported audit seam still rejects that same file
FAKE_HOME="$TMP/home"
mkdir -p "$FAKE_HOME/.claude"
write_tuning "$FAKE_HOME/.claude/build-remote.json" '{ "enabled": true, "hosts": ["ghost"] }'
env -u BUILD_REMOTE_CONFIG HOME="$FAKE_HOME" node --input-type=module -e '
import assert from "node:assert/strict";
import { auditBuildTuning, FleetError, loadFleet } from "'"$ROOT"'/lib/fleet/loader.mjs";
const fleet = loadFleet();
assert.deepEqual(fleet.nodeNames(), ["workstation", "debian1"]);
assert.throws(() => auditBuildTuning(new Set(fleet.nodeNames())), FleetError);
' >"$TMP/herm.out" 2>"$TMP/herm.err"
if [[ $? -eq 0 ]]; then
  ok 'loadFleet reads no build-remote.json; the audit seam still rejects a leftover hosts key'
else
  bad "loadFleet reads no build-remote.json; the audit seam still rejects a leftover hosts key ($(tr '\n' ' ' <"$TMP/herm.err"))"
fi

printf 'passed=%d failed=%d\n' "$pass" "$fail"
[[ $fail -eq 0 ]]
