#!/usr/bin/env bash
# Regression harness for issue #869 Point-1 C2 — the install-ordering guard
# wired into mosaic-link-runtime-assets.
#
# Root cause under test: mosaic-link-runtime-assets copies
# runtime/claude/settings.json (which embeds the PreToolUse mutator-gate.py
# hook and the Stop receipt-observer-client.py hook) straight into
# ~/.claude/settings.json, unconditionally. If the lease-broker activation
# half cannot be confirmed on this host, wiring those hooks bricks it with a
# fail-closed gate that can never be satisfied.
#
# This harness never invokes a real `mosaic` CLI build — it stubs the
# `__link-claude-settings` contract with a fake `mosaic` on PATH so the shell
# WIRING (does mosaic-link-runtime-assets call out correctly? does it
# propagate a degraded outcome? does it still copy every other runtime file?
# does --allow-inactive-enforcement forward through?) is exercised
# independently of the TS guard's own logic (already covered by
# install-ordering-guard.spec.ts). It also exercises the no-mosaic-on-PATH
# python3 fallback directly.
#
# Scenarios:
#   1. probe=true (fake mosaic exits 0)  -> settings.json copied, script exits 0.
#   2. probe=false (fake mosaic exits 1) -> script exits 1 (guard_degraded
#      propagated), but every OTHER runtime file is still copied.
#   3. probe=false + --allow-inactive-enforcement -> the flag is forwarded to
#      the fake mosaic stub.
#   4. No `mosaic` on PATH at all (activation unconfirmable) -> the python3
#      fallback strips the enforcement hooks itself and the script exits 1.
#   5. No `mosaic` on PATH + --allow-inactive-enforcement -> the python3
#      fallback wires the hooks AS-IS and the script exits 0.

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LINK_SCRIPT="$SCRIPT_DIR/mosaic-link-runtime-assets"

TMP_ROOT=$(mktemp -d)
trap 'rm -rf "$TMP_ROOT"' EXIT

fail=0
fail_msg() {
    echo "FAIL: $*" >&2
    fail=1
}

FIXTURE_SETTINGS='{
  "model": "opus",
  "hooks": {
    "PreToolUse": [
      { "matcher": ".*", "hooks": [ { "type": "command", "command": "python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude" } ] },
      { "matcher": "Write|Edit|MultiEdit", "hooks": [ { "type": "command", "command": "~/.config/mosaic/tools/qa/prevent-memory-write.sh" } ] }
    ],
    "Stop": [
      { "hooks": [
        { "type": "command", "command": "python3 ~/.config/mosaic/tools/lease-broker/receipt-observer-client.py --runtime claude" },
        { "type": "command", "command": "~/.config/mosaic/tools/qa/reflect-stop-hook.sh" }
      ] }
    ]
  }
}'

# Sets up a fresh $MOSAIC_HOME/runtime/claude/{settings.json,CLAUDE.md,
# hooks-config.json,context7-integration.md} + fresh $HOME, echoes both paths
# space-separated for the caller to `read`.
new_scenario_dirs() {
    local scenario="$1"
    local base="$TMP_ROOT/$scenario"
    local mosaic_home="$base/mosaic-home"
    local home="$base/home"
    mkdir -p "$mosaic_home/runtime/claude" "$home"
    printf '%s' "$FIXTURE_SETTINGS" > "$mosaic_home/runtime/claude/settings.json"
    echo "claude.md fixture" > "$mosaic_home/runtime/claude/CLAUDE.md"
    echo '{"hooks":{}}' > "$mosaic_home/runtime/claude/hooks-config.json"
    echo "context7 fixture" > "$mosaic_home/runtime/claude/context7-integration.md"
    echo "$mosaic_home" "$home"
}

settings_has_marker() {
    local file="$1" marker="$2"
    [[ -f "$file" ]] && grep -q "$marker" "$file"
}

# A fake `mosaic` binary implementing only the __link-claude-settings contract
# this harness needs: writes dest verbatim (fixture is unmodified either way —
# this stub only exercises the CALL CONTRACT, not the TS strip logic, which
# has its own vitest coverage) and exits with the code the scenario wants.
# Records the args it was called with so the harness can assert forwarding.
make_fake_mosaic() {
    local bin_dir="$1" exit_code="$2"
    mkdir -p "$bin_dir"
    cat > "$bin_dir/mosaic" <<EOF
#!/usr/bin/env bash
set -euo pipefail
echo "\$@" > "$bin_dir/mosaic.args"
if [[ "\$1" == "__link-claude-settings" ]]; then
  cp "\$2" "\$3"
  exit $exit_code
fi
exit 0
EOF
    chmod +x "$bin_dir/mosaic"
}

# --- Scenario 1: probe=true (fake mosaic exits 0) ---------------------------
read -r MOSAIC_HOME_1 HOME_1 < <(new_scenario_dirs scenario1)
BIN_1="$TMP_ROOT/scenario1/bin"
make_fake_mosaic "$BIN_1" 0

OUTPUT=$(MOSAIC_HOME="$MOSAIC_HOME_1" HOME="$HOME_1" PATH="$BIN_1:$PATH" "$LINK_SCRIPT" 2>&1)
STATUS=$?
[[ "$STATUS" -eq 0 ]] || fail_msg "scenario1 (probe=true): expected exit 0, got $STATUS. Output: $OUTPUT"
[[ -f "$HOME_1/.claude/settings.json" ]] || fail_msg "scenario1: settings.json was not copied"

# --- Scenario 2: probe=false (fake mosaic exits 1) --------------------------
read -r MOSAIC_HOME_2 HOME_2 < <(new_scenario_dirs scenario2)
BIN_2="$TMP_ROOT/scenario2/bin"
make_fake_mosaic "$BIN_2" 1

OUTPUT=$(MOSAIC_HOME="$MOSAIC_HOME_2" HOME="$HOME_2" PATH="$BIN_2:$PATH" "$LINK_SCRIPT" 2>&1)
STATUS=$?
[[ "$STATUS" -ne 0 ]] || fail_msg "scenario2 (probe=false, default): expected non-zero exit, got 0. Output: $OUTPUT"
[[ -f "$HOME_2/.claude/CLAUDE.md" ]] || fail_msg "scenario2: CLAUDE.md was NOT copied even though it is independent of the settings.json guard"
[[ -f "$HOME_2/.claude/hooks-config.json" ]] || fail_msg "scenario2: hooks-config.json was NOT copied"
[[ -f "$HOME_2/.claude/context7-integration.md" ]] || fail_msg "scenario2: context7-integration.md was NOT copied"
case "$OUTPUT" in
    *"NOT be wired"*|*"NOT wired"*) ;;
    *) fail_msg "scenario2: expected an actionable degraded-wiring message in output, got: $OUTPUT" ;;
esac

# --- Scenario 3: probe=false + --allow-inactive-enforcement forwards the flag
read -r MOSAIC_HOME_3 HOME_3 < <(new_scenario_dirs scenario3)
BIN_3="$TMP_ROOT/scenario3/bin"
make_fake_mosaic "$BIN_3" 0

MOSAIC_HOME="$MOSAIC_HOME_3" HOME="$HOME_3" PATH="$BIN_3:$PATH" "$LINK_SCRIPT" --allow-inactive-enforcement >/dev/null 2>&1
RECORDED_ARGS="$(cat "$BIN_3/mosaic.args" 2>/dev/null || true)"
case "$RECORDED_ARGS" in
    *"--allow-inactive-enforcement"*) ;;
    *) fail_msg "scenario3: --allow-inactive-enforcement was not forwarded to the mosaic CLI invocation (got: '$RECORDED_ARGS')" ;;
esac

# --- Scenario 4: no `mosaic` on PATH at all -> python3 fallback strips hooks
read -r MOSAIC_HOME_4 HOME_4 < <(new_scenario_dirs scenario4)
EMPTY_BIN="$TMP_ROOT/scenario4/empty-bin"
mkdir -p "$EMPTY_BIN"
# A PATH containing only python3 (for the fallback) + core utils, no mosaic.
FALLBACK_PATH="$EMPTY_BIN:/usr/bin:/bin"

OUTPUT=$(MOSAIC_HOME="$MOSAIC_HOME_4" HOME="$HOME_4" PATH="$FALLBACK_PATH" "$LINK_SCRIPT" 2>&1)
STATUS=$?
[[ "$STATUS" -ne 0 ]] || fail_msg "scenario4 (no mosaic on PATH, default): expected non-zero exit, got 0. Output: $OUTPUT"
if settings_has_marker "$HOME_4/.claude/settings.json" "mutator-gate.py"; then
    fail_msg "scenario4: mutator-gate.py hook was wired even though mosaic could not be resolved (activation unconfirmable)"
fi
if settings_has_marker "$HOME_4/.claude/settings.json" "receipt-observer-client.py"; then
    fail_msg "scenario4: receipt-observer-client.py hook was wired even though mosaic could not be resolved"
fi
if ! settings_has_marker "$HOME_4/.claude/settings.json" "prevent-memory-write.sh"; then
    fail_msg "scenario4: the unrelated prevent-memory-write.sh hook was incorrectly dropped too"
fi

# --- Scenario 5: no `mosaic` on PATH + --allow-inactive-enforcement --------
read -r MOSAIC_HOME_5 HOME_5 < <(new_scenario_dirs scenario5)

OUTPUT=$(MOSAIC_HOME="$MOSAIC_HOME_5" HOME="$HOME_5" PATH="$FALLBACK_PATH" "$LINK_SCRIPT" --allow-inactive-enforcement 2>&1)
STATUS=$?
[[ "$STATUS" -eq 0 ]] || fail_msg "scenario5 (no mosaic, opt-out): expected exit 0, got $STATUS. Output: $OUTPUT"
if ! settings_has_marker "$HOME_5/.claude/settings.json" "mutator-gate.py"; then
    fail_msg "scenario5: mutator-gate.py hook should have been wired (explicit opt-out set)"
fi
case "$OUTPUT" in
    *"WARNING"*"--allow-inactive-enforcement"*) ;;
    *) fail_msg "scenario5: expected a loud WARNING mentioning --allow-inactive-enforcement, got: $OUTPUT" ;;
esac

if [[ "$fail" -eq 0 ]]; then
    echo "install-ordering-guard regression passed (5/5 scenarios)"
fi

exit "$fail"
