#!/usr/bin/env bash
#
# verify-gate.sh — the pre-commit gate for the ReportingProxy / CodeRunner test scope.
#
# One command, one verdict. Run it before every commit that touches app/ or tests/.
#
#   bash scripts/verify-gate.sh            # lint + full suite + coverage floors  (~2.5 min)
#   bash scripts/verify-gate.sh --fast     # lint + suite, no coverage            (~1 min)
#   bash scripts/verify-gate.sh --lint     # syntax check only                    (~1 sec)
#
# --fast skips the coverage report, so it does NOT satisfy the gate. Use it while iterating;
# run the full gate before you commit.
#
# On success the full run writes build/.verify-ok recording a fingerprint of the in-scope
# source and test files. scripts/hooks/commit-gate.php reads that marker to tell whether the
# tree has changed since the last green run.
#
# Exit codes: 0 = green, 1 = a check failed, 2 = the gate could not run (missing DB, etc).

set -u
export LC_ALL=C

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT" || exit 2

MODE="full"
for arg in "$@"; do
	case "$arg" in
		--fast) MODE="fast" ;;
		--lint) MODE="lint" ;;
		-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
		*) printf 'verify-gate: unknown option %s\n' "$arg" >&2; exit 2 ;;
	esac
done

if [ -t 1 ]; then
	RED=$'\033[31m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; BOLD=$'\033[1m'; OFF=$'\033[0m'
else
	RED=''; GREEN=''; YELLOW=''; BOLD=''; OFF=''
fi

step()  { printf '\n%s==> %s%s\n' "$BOLD" "$1" "$OFF"; }
pass()  { printf '%s  ok%s  %s\n' "$GREEN" "$OFF" "$1"; }
fail()  { printf '%sFAIL%s  %s\n' "$RED" "$OFF" "$1"; }
warn()  { printf '%swarn%s  %s\n' "$YELLOW" "$OFF" "$1"; }

FAILED=0

# ---------------------------------------------------------------------------
# 1. Prerequisites
# ---------------------------------------------------------------------------
step "Prerequisites"

if [ ! -x vendor/bin/phpunit ]; then
	fail "vendor/bin/phpunit is missing - run 'composer install'"
	exit 2
fi
pass "phpunit present"

if [ "$MODE" != "lint" ]; then
	# The suite needs a real MySQL database: the sqlite3 extension is not installed on this
	# box, so phpunit.xml.dist's in-memory option is unusable. Credentials live in .env under
	# database.tests.*; the schema is built from the App migrations on first run.
	if ! grep -qE '^[[:space:]]*database\.tests\.database' .env 2>/dev/null; then
		fail "no database.tests.* block in .env - see .claude/skills/test-run/SKILL.md"
		exit 2
	fi
	pass "test database configured in .env"
fi

# ---------------------------------------------------------------------------
# 2. Syntax check on everything the working tree has touched
# ---------------------------------------------------------------------------
step "Syntax (php -l on changed files)"

CHANGED="$(
	{
		git diff --name-only --diff-filter=d HEAD -- '*.php' 2>/dev/null
		git ls-files --others --exclude-standard -- '*.php' 2>/dev/null
	} | sort -u
)"

if [ -z "$CHANGED" ]; then
	pass "no changed PHP files"
else
	LINT_FAILED=0
	while IFS= read -r file; do
		[ -f "$file" ] || continue
		if ! out="$(php -l "$file" 2>&1)"; then
			fail "$file"
			printf '%s\n' "$out" | sed 's/^/      /'
			LINT_FAILED=1
		fi
	done <<< "$CHANGED"
	if [ "$LINT_FAILED" -eq 0 ]; then
		pass "$(printf '%s\n' "$CHANGED" | grep -c .) changed PHP file(s) parse cleanly"
	else
		FAILED=1
	fi
fi

if [ "$MODE" = "lint" ]; then
	step "Verdict"
	if [ "$FAILED" -eq 0 ]; then pass "lint only - the gate is NOT satisfied"; exit 0; fi
	fail "lint failed"; exit 1
fi

# ---------------------------------------------------------------------------
# 3. The suite
# ---------------------------------------------------------------------------
step "Test suite"

PHPUNIT_ARGS=()
if [ "$MODE" = "fast" ]; then
	PHPUNIT_ARGS+=(--no-coverage)
	warn "coverage disabled (--fast) - floors will not be checked"
fi

SUITE_LOG="$(mktemp -t verify-gate-suite.XXXXXX)"
trap 'rm -f "$SUITE_LOG"' EXIT

if vendor/bin/phpunit "${PHPUNIT_ARGS[@]}" > "$SUITE_LOG" 2>&1; then
	pass "$(grep -oE 'OK \([0-9]+ tests, [0-9]+ assertions\)' "$SUITE_LOG" | tail -1)"
else
	fail "the suite is red"
	# Show the failure detail, not the 20 lines of progress dots before it.
	sed -n '/^There \(was\|were\) [0-9]* \(failure\|error\)/,$p' "$SUITE_LOG" | head -80
	if ! grep -q '^There ' "$SUITE_LOG"; then
		tail -40 "$SUITE_LOG"
	fi
	FAILED=1
fi

# beStrictAboutOutputDuringTests and failOnRisky are on, so a risky test is already a
# non-zero exit. Surface the count anyway - it is the single most common surprise here.
if grep -qE '^(OK, but|Tests:.*Risky)' "$SUITE_LOG"; then
	warn "risky tests reported - a test echoed output or left a DB handle open"
fi

# ---------------------------------------------------------------------------
# 4. Coverage floors
# ---------------------------------------------------------------------------
if [ "$MODE" = "full" ]; then
	step "Coverage floors"
	if php scripts/coverage-floor.php; then
		pass "every in-scope file is at or above its floor"
	else
		fail "coverage regressed - new code landed without tests"
		FAILED=1
	fi
fi

# ---------------------------------------------------------------------------
# 5. Seam budgets — keep the untestable constructs out
# ---------------------------------------------------------------------------
step "Seam budgets"

if php scripts/seam-budget.php; then
	pass "no new exit/die, sleep, or raw cURL in the tested scope"
else
	fail "an untestable construct was added - route it through the seam"
	FAILED=1
fi

# ---------------------------------------------------------------------------
# 6. Hygiene the suite cannot see
# ---------------------------------------------------------------------------
step "Hygiene"

# writable/db_backups is tracked in git, and BaseModel's backup tests write into it. A leftover
# fixture means a tearDown did not clean up and would otherwise be committed by accident.
STRAY="$(git status --porcelain -- writable/db_backups 2>/dev/null)"
if [ -n "$STRAY" ]; then
	fail "leftover files in writable/db_backups - a backup test did not clean up:"
	printf '%s\n' "$STRAY" | sed 's/^/      /'
	FAILED=1
else
	pass "writable/db_backups is clean"
fi

# The suite must never reach the network. DeniedTransport in tests/bootstrap.php is what
# guarantees that; losing it means a test run can create cards on the live Rock SA board.
if grep -q 'DeniedTransport' tests/bootstrap.php 2>/dev/null \
	&& grep -q 'OutboundNotifications::intercept' tests/bootstrap.php 2>/dev/null; then
	pass "outbound guards intact in tests/bootstrap.php"
else
	fail "tests/bootstrap.php no longer installs DeniedTransport + OutboundNotifications::intercept"
	fail "  without both, a failing test can post to production - restore them before committing"
	FAILED=1
fi

# ---------------------------------------------------------------------------
# 7. Verdict
# ---------------------------------------------------------------------------
step "Verdict"

if [ "$FAILED" -ne 0 ]; then
	fail "gate is RED - do not commit"
	exit 1
fi

if [ "$MODE" = "full" ]; then
	mkdir -p build
	php scripts/hooks/commit-gate.php --write-marker
	pass "gate is GREEN - marker written to build/.verify-ok"
else
	pass "suite is green, but --fast does not satisfy the gate (no coverage check)"
fi

exit 0
