#!/usr/bin/env bash
# beagle-ci: gate script — runs everything that must pass before a commit ships.
#
# This is not a fast check, and the number that said it was (~25s for layer 1)
# was wrong by two orders of magnitude — wrong in the direction that makes
# someone start it expecting to wait, then assume it hung. Layer 1 is TENS OF
# MINUTES cold (order 10-30, depending on how much of the native path has to
# build) and seconds warm, once the gate-result cache can replay every eligible
# file. Nothing here is timed against a fixed budget; the honest expectation is
# "cold is a coffee, warm is a breath", and a cold run that seems stuck is
# usually just cold.
#
# Layers (in order, fastest first):
#   1. raco test over the blocking files in beagle-test/tests/ (tens of minutes
#       cold, seconds warm — the gate-result cache decides which, per file)
#   2. bin/test/gate-cache/run.sh             (gate-result cache under-key
#       regression tests — the cache must re-run when any traced input
#       changes; seconds, self-skipping under an outer trace)
#   3. lint pass over fixtures                (catches stdlib drift)
#   4. (optional) firn-build + firn-validate against ~/code/nixos-config
#       — enabled with BEAGLE_VALIDATE_NIXOS_CONFIG=1
#   5. (optional) bin/beagle-downstream --run — hermetic five-consumer gate
#       (gjoa/wake/north/store/nixos-config compiled with candidate ~/code/beagle
#       into external scratch; byte-clean by construction) — enabled with
#       BEAGLE_DOWNSTREAM_GATE=1. Fast local `bin/beagle-ci` runs with this OFF
#       by default (the five-consumer compile is a further cold build on top of
#       everything above, and never replays from the gate-result cache); the
#       release-verification path must set BEAGLE_DOWNSTREAM_GATE=1 so this
#       layer is load-bearing there. BEAGLE_DOWNSTREAM_REGISTRY/_CONSUMER/
#       _ALLOW_DIRTY forward to bin/beagle-downstream for probes/fixtures.
#
#       Receipt lifecycle: the layer-5 receipt is EPHEMERAL by default — it
#       lives inside a per-run owned temp root and is reclaimed on EVERY exit
#       path (success, forced consumer failure, timeout, planted exception,
#       SIGINT, SIGTERM), so repeated default gate runs leave zero new receipt
#       or temp roots on disk. To keep a durable, schema-valid
#       (beagle-downstream/1) receipt, set BEAGLE_DOWNSTREAM_RECEIPT=<path> —
#       that caller-owned destination is written and never reclaimed here.
#       Cleanup is exact and identity-scoped (removes only this run's own root
#       by literal path); a concurrent gate run's root is never touched.
#
# Exits non-zero if any layer fails.

set -euo pipefail

source "$(dirname "$0")/_beagle-cold-authority"

repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo"
source "$(dirname "${BASH_SOURCE[0]}")/_beagle-racket"

# The release preflight is the only caller that opts into the extended Beagle
# tier. Ordinary `bin/beagle-test` remains the fast local path; the existing
# downstream-gate switch is the release-bar contract and is already required
# by release verification.
if [[ "${BEAGLE_DOWNSTREAM_GATE:-0}" == "1" ]]; then
    echo "=== release preflight: extended Beagle tier ==="
    bin/beagle-test --full --include-gated
fi

echo "=== 1/5: raco test blocking beagle-test/tests/ ==="
blocking_tests=()
for test_file in beagle-test/tests/*.rkt; do
    # The v0.24.0 preflight already runs this flaky suite in the advisory tier.
    if [[ "$test_file" == "beagle-test/tests/native-c17-parallel.rkt" ]]; then
        continue
    fi
    blocking_tests+=("$test_file")
done
"$RACO" test "${blocking_tests[@]}" 2>&1
echo "  native-c17-parallel.rkt ran in the extended advisory tier (v0.24.0 seal only)"

echo
echo "=== 2/5: gate-result cache under-key regression tests ==="
bin/test/gate-cache/run.sh 2>&1

echo
echo "=== 3/5: nix-instantiate smoke test on all fixtures ==="
if command -v nix-instantiate >/dev/null; then
    BEAGLE_NIX_EVAL_CHECK=1 "$RACO" test beagle-test/tests/nix-property.rkt 2>&1
else
    echo "  (skipped — nix-instantiate not on PATH)"
fi

if [[ "${BEAGLE_VALIDATE_NIXOS_CONFIG:-0}" == "1" ]]; then
    echo
    echo "=== 4/5: nixos-config firn-build + firn-validate ==="
    nixos_repo="${NIXOS_CONFIG_REPO:-$HOME/code/nixos-config/main}"
    if [[ ! -d "$nixos_repo" ]]; then
        echo "  (skipped — $nixos_repo not found; set NIXOS_CONFIG_REPO=<path>)"
    else
        cd "$nixos_repo"
        BEAGLE_PATH="$repo" ./scripts/firn-build 2>&1
        BEAGLE_PATH="$repo" ./scripts/firn-validate 2>&1
    fi
else
    echo
    echo "=== 4/5: nixos-config validate (skipped — set BEAGLE_VALIDATE_NIXOS_CONFIG=1) ==="
fi

if [[ "${BEAGLE_DOWNSTREAM_GATE:-0}" == "1" ]]; then
    echo
    echo "=== 5/5: bin/beagle-downstream — hermetic five-consumer downstream gate ==="

    # Per-run OWNED temp root. The default (ephemeral) receipt lives inside it,
    # so reclamation is a single literal-path removal of OUR root only — never
    # a glob over the shared prefix, so a concurrent gate run's root survives.
    ci_downstream_root="$(mktemp -d -t beagle-ci-downstream.XXXXXX)"

    # Receipt destination. An explicit caller-owned path (BEAGLE_DOWNSTREAM_RECEIPT)
    # is DURABLE — written by the gate (schema beagle-downstream/1) and never
    # reclaimed here. Absent it, the receipt is ephemeral inside the owned root
    # and dies with it on every exit path.
    if [[ -n "${BEAGLE_DOWNSTREAM_RECEIPT:-}" ]]; then
        downstream_receipt="$BEAGLE_DOWNSTREAM_RECEIPT"
        downstream_receipt_durable=1
    else
        downstream_receipt="$ci_downstream_root/receipt.json"
        downstream_receipt_durable=0
    fi

    downstream_child=""
    downstream_sigtarget=""
    _ci_downstream_reclaim() {
        # Reap OUR whole downstream SUBTREE (the gate + every per-consumer
        # compile it spawned — an orphaned grandchild is still an owned child)
        # with a graceful INT so the gate's own scratch dynamic-wind unwinds,
        # then remove ONLY our owned root by literal path. `:?` aborts on an
        # empty/unset root rather than ever `rm -rf`-ing bare.
        if [[ -n "$downstream_child" ]] && kill -0 "$downstream_child" 2>/dev/null; then
            kill -INT "$downstream_sigtarget" 2>/dev/null || true
            wait "$downstream_child" 2>/dev/null || true
        fi
        rm -rf "${ci_downstream_root:?}"
    }
    _ci_downstream_on_signal() { # <signal-name> <signal-number>
        _ci_downstream_reclaim
        trap - EXIT INT TERM
        kill -"$1" "$$" 2>/dev/null || true    # re-raise -> truthful 128+n exit
        exit $((128 + $2))
    }
    trap '_ci_downstream_reclaim' EXIT
    trap '_ci_downstream_on_signal INT 2'  INT
    trap '_ci_downstream_on_signal TERM 15' TERM

    downstream_args=(--run --json "$downstream_receipt")
    [[ -n "${BEAGLE_DOWNSTREAM_REGISTRY:-}" ]] && downstream_args+=(--registry "$BEAGLE_DOWNSTREAM_REGISTRY")
    [[ -n "${BEAGLE_DOWNSTREAM_CONSUMER:-}" ]] && downstream_args+=(--consumer "$BEAGLE_DOWNSTREAM_CONSUMER")
    [[ -n "${BEAGLE_DOWNSTREAM_ALLOW_DIRTY:-}" ]] && downstream_args+=(--allow-dirty)

    # Backgrounded + waited (not foreground): bash defers traps until a
    # FOREGROUND child returns, so a directed SIGINT/SIGTERM to this script
    # would otherwise never run our cleanup nor forward to the child. With
    # `wait`, a signal interrupts it, the trap fires, the subtree is forwarded
    # + reaped, and our root is reclaimed. `setsid` puts the gate in its own
    # process group so reclamation can INT the WHOLE subtree (`-PGID`) — the
    # gate never shares a group with this script, so we never self-signal.
    # Exit code still propagates undisguised (no `| tail`): this is the release
    # path's load-bearing gate.
    if command -v setsid >/dev/null 2>&1; then
        setsid bin/beagle-downstream "${downstream_args[@]}" &
        downstream_child=$!
        downstream_sigtarget="-$downstream_child"   # negative == its process group
    else
        bin/beagle-downstream "${downstream_args[@]}" &
        downstream_child=$!
        downstream_sigtarget="$downstream_child"
    fi
    downstream_status=0
    wait "$downstream_child" || downstream_status=$?
    downstream_child=""
    if [[ "$downstream_receipt_durable" == "1" ]]; then
        echo "  receipt -> $downstream_receipt"
    else
        echo "  receipt -> $downstream_receipt (ephemeral; reclaimed on exit)"
    fi
    if [[ "$downstream_status" -ne 0 ]]; then
        exit "$downstream_status"
    fi
else
    echo
    echo "=== 5/5: downstream consumer gate (skipped — set BEAGLE_DOWNSTREAM_GATE=1) ==="
fi

echo
echo "beagle-ci: ✓ all layers passed"
