#!/usr/bin/env bash
# LaneKeep pre-push hook — runs targeted bats tests before pushing.
# Install via: bash scripts/setup-dev-hooks.sh
#
# Skip entirely:  SKIP_TESTS=1 git push
# Full suite:     FULL_TESTS=1 git push
set -euo pipefail

if [[ "${SKIP_TESTS:-0}" == "1" ]]; then
  echo "[pre-push] SKIP_TESTS=1 — skipping tests." >&2
  exit 0
fi

# Dependency check
if ! command -v bats >/dev/null 2>&1; then
  echo "[pre-push] bats not found. Install: https://github.com/bats-core/bats-core" >&2
  echo "[pre-push] Or skip with: SKIP_TESTS=1 git push" >&2
  exit 1
fi

REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"

# Collect changed files between upstream and HEAD (falls back to all tracked
# files when there is no upstream, e.g. first push of a new branch)
if git rev-parse --verify @{u} >/dev/null 2>&1; then
  changed=$(git diff --name-only @{u}..HEAD)
else
  changed=$(git diff --name-only HEAD~1..HEAD 2>/dev/null || git ls-files)
fi

if [[ "${FULL_TESTS:-0}" == "1" ]]; then
  changed="FORCE_FULL"
fi

# Fast-path for docs/harness-only commits (README, CLAUDE.md, .claude/…, SHA
# pins). No test suite covers these files, and the "no suite matched → full
# suite" fallback below takes 60+s — long enough for GitHub to close the SSH
# push connection idle-side. Skip tests when nothing test-relevant changed.
# Force full via FULL_TESTS=1 if you want the full suite anyway.
if [[ "$changed" != "FORCE_FULL" ]] && [[ -n "$changed" ]] \
  && ! echo "$changed" | grep -qvE '(\.md$|\.sha256$|^\.claude/|^CLAUDE\.md$|^README\.md$|^SECURITY\.md$|^CONTRIBUTING\.md$|^docs/)'; then
  echo "[pre-push] Only docs/harness files changed — skipping tests." >&2
  echo "[pre-push] (Force full suite with FULL_TESTS=1 git push)" >&2
  exit 0
fi

# Path → test directory mapping
declare -A suite_map=(
  ["defaults/"]="tests/rules/"
  ["lib/eval-"]="tests/evaluators/ tests/pipeline/"
  ["lib/config.sh"]="tests/config/"
  ["lib/cumulative.sh"]="tests/pipeline/"
  ["lib/dispatcher.sh"]="tests/pipeline/"
  ["lib/hooks.sh"]="tests/hooks/"
  ["bin/"]="tests/cli/ tests/hooks/"
  ["hooks/"]="tests/hooks/"
  ["plugins.d/"]="tests/plugins/"
  ["lib/"]="tests/pipeline/ tests/evaluators/"
  ["tests/"]="SELF"
)

selected_suites=""

for pattern in "${!suite_map[@]}"; do
  if echo "$changed" | grep -q "$pattern"; then
    suite="${suite_map[$pattern]}"
    if [[ "$suite" == "SELF" ]]; then
      # Test files changed — run only the specific changed test files
      for f in $(echo "$changed" | grep '^tests/.*\.bats$'); do
        [[ -f "$f" ]] && selected_suites="$selected_suites $f"
      done
    else
      selected_suites="$selected_suites $suite"
    fi
  fi
done

# Deduplicate and clean up
if [[ -n "$selected_suites" && "$changed" != "FORCE_FULL" ]]; then
  # Build unique list, dropping individual files whose parent dir is already included
  unique_suites=""
  for entry in $(echo "$selected_suites" | tr ' ' '\n' | sort -u); do
    # Skip individual .bats files when their containing directory is already listed
    if [[ "$entry" == *.bats ]]; then
      parent_dir="$(dirname "$entry")/"
      if echo "$selected_suites" | tr ' ' '\n' | grep -qxF "$parent_dir"; then
        continue
      fi
    fi
    unique_suites="$unique_suites $entry"
  done
  echo "[pre-push] Running targeted tests: $unique_suites" >&2
  bats_args="$unique_suites"
else
  echo "[pre-push] Running full test suite..." >&2
  bats_args="--recursive tests/"
fi

# Run bats and capture both exit code and output. Don't `exec` — we need to
# post-process the exit code because bats 1.13's TAP formatter occasionally
# returns 141 (SIGPIPE) on cleanup even when every test passed; with `set -e`
# (or git treating non-zero as hook failure) that aborts the push spuriously.
# We trust the TAP output ("not ok") as ground truth and translate a clean
# 141 to 0; any real failure still propagates.
log=$(mktemp)
trap 'rm -f "$log"' EXIT

set +e
# shellcheck disable=SC2086
bats $bats_args 2>&1 | tee "$log"
rc=${PIPESTATUS[0]}
set -e

if [ "$rc" -eq 141 ] && ! grep -q '^not ok' "$log"; then
  echo "[pre-push] bats exited 141 (SIGPIPE) but no 'not ok' in output — treating as success." >&2
  exit 0
fi

exit "$rc"
