#!/usr/bin/env bash
# Run every gate CI runs, in the same order, and report all failures.
#
# Usage: bin/check [--only <gate>] [--list]
#
# This is the single definition of the gate commands. A contributor runs it
# before committing, bin/publish runs it before releasing, and CI runs one gate
# per job with --only, so the commands live in exactly one place.
#
# CI runs the gates as independent jobs so one failure does not hide the rest.
# This script does the same locally: it does not stop at the first failure, so
# one run gives the whole list instead of one round-trip per gate.
#
# The test gate is absent from CI's use of this script. CI runs pytest over a
# 3.11/3.13 matrix, which a single local run cannot reproduce, so that job
# spells out its own command.
set -uo pipefail

# -e is deliberately absent: a gate that fails must not stop the ones after it.
# That makes this cd unguarded by default, and a failed cd would run every gate
# against whatever directory the caller happened to be in.
cd "$(dirname "$0")/.." || exit 2

# Gate name, then the command it runs. This is the only list of gate names:
# ALL_GATES is derived from it below, and CI builds its matrix from --list, so
# adding a gate here is the whole change.
#
# The command goes into an array rather than a string. A string would need word
# splitting to become argv, which silently mangles any argument containing a
# space -- so the first gate needing `--ignore "a pattern"` would run the wrong
# command instead of failing.
gate_command() {
    case "$1" in
        format) cmd=(uv run ruff format --check .) ;;
        lint) cmd=(uv run ruff check .) ;;
        typecheck) cmd=(uv run pyright src/ tests/) ;;
        deadcode) cmd=(uv run vulture) ;;
        test) cmd=(uv run pytest -q) ;;
        # --warn makes coverage drift visible. It cannot change the exit code,
        # so the gate still fails only on referential integrity -- but a spec
        # point that loses its test now says so instead of printing OK.
        spec-check) cmd=(uv run spec-trace --warn) ;;
        *) return 1 ;;
    esac
}

# Kept in this order: format and lint are the fastest, so a bare run reports the
# cheapest failures first.
ALL_GATES=(format lint typecheck deadcode test spec-check)

only=""
while [ $# -gt 0 ]; do
    case "$1" in
        --only)
            only="${2-}"
            if [ -z "$only" ]; then
                echo "Error: --only needs a gate name" >&2
                exit 2
            fi
            shift 2
            ;;
        --list)
            printf '%s\n' "${ALL_GATES[@]}"
            exit 0
            ;;
        -h | --help)
            # Print the header block, stopping at the first non-comment line,
            # so editing the preamble cannot truncate the help text.
            awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0"
            exit 0
            ;;
        *)
            echo "Error: unknown argument: $1" >&2
            exit 2
            ;;
    esac
done

if [ -n "$only" ]; then
    if ! gate_command "$only" >/dev/null; then
        echo "Error: unknown gate: ${only}" >&2
        echo "Known gates: ${ALL_GATES[*]}" >&2
        exit 2
    fi
    gates=("$only")
else
    gates=("${ALL_GATES[@]}")
fi

failed=()

for name in "${gates[@]}"; do
    echo "==> ${name}"
    # Look the command up before running it. An unknown name would otherwise
    # leave `cmd` holding the previous gate's command, so a typo in ALL_GATES
    # would run that gate twice and report both as passing.
    cmd=()
    if ! gate_command "$name"; then
        echo "Error: unknown gate: ${name}" >&2
        exit 2
    fi
    if ! "${cmd[@]}"; then
        failed+=("${name}")
    fi
    echo
done

if [ ${#failed[@]} -gt 0 ]; then
    echo "FAILED: ${failed[*]}"
    exit 1
fi

echo "All gates passed."
