#!/usr/bin/env bash
# The fleet payload's settings.json must name only paths a box actually has: workstation
# locations are rewritten to their box-side location, and anything left unaccounted for
# aborts staging instead of shipping a hook Claude Code would skip without a word.
set -uo pipefail

MOD="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
root="$(mktemp -d)"
trap 'rm -rf "$root"' EXIT

fail=0
t() { # name expected actual
  if [ "$2" = "$3" ]; then printf 'ok    %s\n' "$1"
  else printf 'FAIL  %s\n      expected: %s\n      actual:   %s\n' "$1" "$2" "$3"; fail=$((fail + 1)); fi
}
has() { # name needle haystack
  case "$3" in *"$2"*) printf 'ok    %s\n' "$1";;
    *) printf 'FAIL  %s\n      wanted substring: %s\n      actual: %s\n' "$1" "$2" "$3"; fail=$((fail + 1));; esac
}

repo="$root/repo"
source_home="$root/workstation"
box_home="$root/box"
target_home="$box_home/.claude"
devtools="$root/devtools.json"
deploy="$root/deploy/modules/workstation/claude"
mkdir -p "$repo" "$deploy/hooks" "$deploy/bin" "$source_home/Projects/slopgate/hooks" "$box_home"
printf '#!/bin/sh\n' >"$deploy/hooks/present.sh"
printf '#!/bin/sh\n' >"$deploy/bin/bun"
printf 'owner instructions\n' >"$repo/claude-home.md"
chmod 755 "$deploy/hooks/present.sh" "$deploy/bin/bun"

# hooks and bin are `source: deploy` in the real manifest — the executed bytes come from
# the landed deploy clone, not the working tree. CLAUDE.md pins the manifest's src override.
cat >"$root/manifest.json" <<'JSON'
{"entries":[{"path":"hooks","source":"deploy"},{"path":"bin","source":"deploy"},{"path":"CLAUDE.md","source":"repo","src":"claude-home.md"},{"path":"settings.json","source":"repo"}]}
JSON

cat >"$devtools" <<'JSON'
{
  "root": "~/.dev-tools",
  "hook_paths": {
    "rewrites": [
      { "from": "/opt/fixture/bun", "to": "{claude_home}/bin/bun" },
      { "from": "~/Projects/slopgate", "to": "{devtools_root}/slopgate" }
    ]
  },
  "tools": [ { "name": "slopgate" } ]
}
JSON

write_settings() { # command
  jq -n --arg command "$1" '{hooks:{PreToolUse:[{hooks:[{type:"command",command:$command}]}]}}' >"$repo/settings.json"
}

with_env() { # command...
  MOD="$MOD" CLAUDE_MANIFEST="$root/manifest.json" CLAUDE_REPO_ROOT="$repo" \
    OVERDECK_DEPLOY_DIR="$root/deploy" CLAUDE_HOME_TARGET="$target_home" \
    CLAUDE_SOURCE_HOME="$source_home" CLAUDE_DEVTOOLS="$devtools" "$@"
}

staged_command() { # -> the single hook command as staged, or nothing on failure
  with_env bash -c '
    . "$1/lib/claude-home.sh"
    stage=$(claude_home_stage) || exit 1
    jq -r ".hooks.PreToolUse[0].hooks[0].command" "$stage/tree/settings.json"
    claude_home_discard_stage "$stage"' _ "$MOD"
}

staged_file() { # payload-relative path
  with_env bash -c '
    . "$1/lib/claude-home.sh"
    stage=$(claude_home_stage) || exit 1
    content=$(<"$stage/tree/$2")
    claude_home_discard_stage "$stage"
    printf "%s" "$content"' _ "$MOD" "$1"
}

stage_error() { # -> stderr of a staging run expected to fail
  with_env bash -c '
    . "$1/lib/claude-home.sh"
    stage=$(claude_home_stage) && { echo "STAGED-ANYWAY"; rm -rf "$stage"; }' _ "$MOD" 2>&1
}

write_settings "bash $target_home/hooks/present.sh"
t "manifest src override maps CLAUDE.md from claude-home.md" \
  "owner instructions" "$(staged_file CLAUDE.md)"
push_body="$(sed -n '/^push_claude_home/,/^}/p' "$MOD/lib/claude-home.sh")"
case "$push_body" in
  *'cp -a "$d/tree/$e" "$HOME/.claude/$e"'*) readonly_copy=1 ;;
  *) readonly_copy=0 ;;
esac
t "remote install copies entries without unlinking read-only staging files" "1" "$readonly_copy"
case "$push_body" in
  *'find "$d" -type d -exec chmod u+rwx {} +'*) remote_cleanup=1 ;;
  *) remote_cleanup=0 ;;
esac
t "remote install reopens extracted directories before cleanup" "1" "$remote_cleanup"
case "$push_body" in
  *'[ ! -L "$HOME/.claude/$e" ]'*'find "$HOME/.claude/$e" -type d -exec chmod u+rwx {} +'*) target_reopen=1 ;;
  *) target_reopen=0 ;;
esac
t "remote install reopens only real destination trees before replacement" "1" "$target_reopen"
t "payload hook survives untouched" \
  "bash $target_home/hooks/present.sh" "$(staged_command)"

write_settings "/opt/fixture/bun $target_home/hooks/present.sh"
t "workstation interpreter is rewritten to the payload wrapper" \
  "$target_home/bin/bun $target_home/hooks/present.sh" "$(staged_command)"

# The regression this file exists for: these hooks are installed on every box by
# devtools.json. Dropping the registration disables the gate fleet-wide, silently.
write_settings "$source_home/Projects/slopgate/hooks/commit-hook.sh"
t "workstation tool checkout is rewritten, never dropped" \
  "$box_home/.dev-tools/slopgate/hooks/commit-hook.sh" "$(staged_command)"

write_settings "bash $target_home/hooks/missing.sh"
chmod 0555 "$deploy/hooks"
missing_error="$(stage_error)"
has "hook missing from the payload aborts staging" \
  "settings hook target missing from payload: $target_home/hooks/missing.sh" "$missing_error"
case "$missing_error" in
  *"Permission denied"*) clean_failure=0 ;;
  *) clean_failure=1 ;;
esac
t "failed staging cleans copied read-only directories" "1" "$clean_failure"
chmod 0755 "$deploy/hooks"

write_settings "$source_home/.fewtok/hooks/read-gate.sh"
has "path with no box-side equivalent aborts staging" \
  "which is neither payload state under $target_home nor a declared devtools path" "$(stage_error)"

write_settings "$box_home/.dev-tools/undeclared/hooks/x.sh"
has "devtools path for an undeclared tool aborts staging" \
  "devtools.json declares no tool 'undeclared'" "$(stage_error)"

# The shared settings.json the fleet actually ships, against the real devtools map. No
# assertion here names this machine: what is checked is that nothing survives preparation
# which a box could not resolve.
real="$root/real-settings.json"
cp "$MOD/../workstation/claude/settings.json" "$real"
unresolved="$(
  MOD="$MOD" bash -c '
    . "$1/lib/claude-home.sh"
    claude_home_prepare_settings "$2" >/dev/null || exit 1
    devtools_root="$(claude_home_devtools_root)"
    claude_home_settings_commands "$2" | while IFS= read -r cmd; do
      read -r -a toks <<<"$cmd"
      for tok in "${toks[@]}"; do
        bare="${tok#[\"\x27]}"; bare="${bare%[\"\x27]}"
        case "$bare" in
          "$CLAUDE_HOME_TARGET"/* | "$devtools_root"/*) ;;
          /*) printf "%s\n" "$bare" ;;
        esac
      done
    done' _ "$MOD" "$real"
)"
t "shipped settings.json resolves entirely to fleet paths" "" "$unresolved"

# The rewritten file must keep the source's mode: mktemp's private 0600 would make
# the staged tree digest-differ from a seat runtime rendered from the same source.
modefile="$root/mode-settings.json"
cp "$MOD/../workstation/claude/settings.json" "$modefile"
chmod 664 "$modefile"
MOD="$MOD" bash -c '. "$1/lib/claude-home.sh"; claude_home_prepare_settings "$2"' _ "$MOD" "$modefile" >/dev/null
t "prepared settings keeps the source mode" "664" "$(stat -c %a "$modefile")"

readonly_modefile="$root/readonly-settings.json"
cp "$MOD/../workstation/claude/settings.json" "$readonly_modefile"
chmod 444 "$readonly_modefile"
MOD="$MOD" bash -c '. "$1/lib/claude-home.sh"; claude_home_prepare_settings "$2"' _ "$MOD" "$readonly_modefile" >/dev/null
readonly_rc=$?
t "read-only settings can be prepared" "0" "$readonly_rc"
t "prepared read-only settings keeps its mode" "444" "$(stat -c %a "$readonly_modefile")"

[ "$fail" = 0 ] || { printf '\n%d failed\n' "$fail"; exit 1; }
printf '\nall passed\n'
