#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Xquik Contributors
# SPDX-License-Identifier: Apache-2.0
#
# Runs every repository check in order and stops at the first failure.
# Usage: bin/check-local [--no-status]
# CodeQL is mandatory, so install it with brew install --cask codeql.
# When the tree is clean and GitHub has HEAD, the result is posted to HEAD
# as the xquik-local-checks commit status, through the gh login of whoever
# runs this script. The status text lists only the checks that passed in
# this run. --no-status skips posting.
set -euo pipefail
cd "$(dirname "$0")/.."

repository="Xquik-dev/prefect-xquik"
status_context="xquik-local-checks"
reuse_version="6.2.0"

post_commit_status=true
for argument in "$@"; do
  case "$argument" in
    --no-status) post_commit_status=false ;;
    *)
      echo "check-local: unknown option $argument. Use --no-status or no option." >&2
      exit 2
      ;;
  esac
done

status_commit=""
if ! $post_commit_status; then
  echo "==> Commit status not posted because --no-status was passed."
elif ! command -v gh >/dev/null; then
  echo "==> Commit status not posted because the GitHub CLI is not installed."
elif [[ -n "$(git status --porcelain)" ]]; then
  echo "==> Commit status not posted because the tree has uncommitted changes."
elif ! gh api --silent "repos/$repository/commits/$(git rev-parse HEAD)" >/dev/null 2>&1; then
  echo "==> Commit status not posted because GitHub does not have HEAD, or gh is signed out. Push first."
else
  status_commit="$(git rev-parse HEAD)"
fi

# Posts the commit status. A failed post prints a warning and keeps the check result.
post_status() {
  local state="$1" description="${2:0:139}"
  gh api --silent "repos/$repository/statuses/$status_commit" \
    -f state="$state" -f context="$status_context" -f description="$description" >/dev/null 2>&1 ||
    echo "check-local: could not post the $state $status_context status. Run gh auth status." >&2
}

workspace="$(mktemp -d "${TMPDIR:-/tmp}/prefect-xquik-check-local.XXXXXX")"
current_check="Setup"
passed=()
summary=""

on_exit() {
  local status=$?
  if [[ -n "$status_commit" ]]; then
    if ((status == 0)) && [[ -n "$summary" ]]; then
      post_status success "Passed $summary"
    else
      post_status failure "Failed at $current_check."
    fi
  fi
  rm -rf -- "${workspace:?}"
}
trap on_exit EXIT

if [[ -n "$status_commit" ]]; then
  echo "==> Posting a pending $status_context status to ${status_commit:0:12}."
  post_status pending "Running licensing, lint, audit, tests, build & CodeQL on 4 Python versions."
fi

# A missing or outdated CodeQL CLI fails here, before the slower checks run.
current_check="CodeQL CLI"
codeql_version="$(bash scripts/codeql-security --version)"

# Runs 1 check and records its label for the status. On failure, prints the
# check name and the next step, then exits. An empty label records nothing.
run_check() {
  local label="$1" name="$2" next_step="$3"
  shift 3
  current_check="$name"
  echo "==> $name"
  "$@" || {
    local status=$?
    echo "check-local: $name failed. $next_step" >&2
    exit "$status"
  }
  [[ -z "$label" ]] || passed+=("$label")
}

# Runs a command on 1 Python version, in an environment made for this run.
on_python() {
  local python_version="$1"
  shift
  UV_PYTHON="$python_version" UV_PROJECT_ENVIRONMENT="$workspace/venv-$python_version" "$@"
}

# Shell functions run without errexit inside run_check, so each one chains its commands with &&.
run_tests() {
  on_python "$1" uv run --no-sync pytest 2>&1 | tee -a "$workspace/tests.log"
}

build_and_check_metadata() {
  on_python "$1" ./scripts/build_reproducibly.sh "$workspace/dist-$1" &&
    on_python "$1" uv run --no-sync twine check "$workspace/dist-$1"/*
}

run_check "REUSE" "Licensing with REUSE lint" \
  "Add the missing copyright and license data listed above." \
  uv tool run --from "reuse==$reuse_version" reuse lint
for python_version in 3.10 3.11 3.12 3.13; do
  run_check "" "Install locked dependencies on Python $python_version" \
    "Install uv and check network access, or update uv.lock, then rerun." \
    on_python "$python_version" uv sync --locked --dev
  run_check "" "Format check on Python $python_version" \
    "Run uv run ruff format . to repair the files listed above." \
    on_python "$python_version" uv run --no-sync ruff format --check .
  run_check "" "Lint on Python $python_version" \
    "Fix the Ruff findings above." on_python "$python_version" uv run --no-sync ruff check .
  run_check "" "Dependency audit on Python $python_version" \
    "Upgrade each dependency named above in uv.lock." on_python "$python_version" uv run --no-sync pip-audit
  run_check "" "Tests on Python $python_version" \
    "Fix the failing tests or the coverage gaps above." run_tests "$python_version"
  run_check "" "Reproducible build and metadata on Python $python_version" \
    "Fix the build, reproducibility or metadata error above." build_and_check_metadata "$python_version"
done
test_summary="$(awk '/^=+ .*[0-9]+ passed/ { for (i = 1; i < NF; i++) if ($(i + 1) ~ /^passed/) { total += $i; runs++ } } END { if (runs) printf "%d passed in %d runs", total, runs }' "$workspace/tests.log")"
passed+=("format, lint, audit, tests${test_summary:+ ($test_summary)}, reproducible build & metadata")
run_check "CodeQL $codeql_version" "Static analysis with CodeQL $codeql_version security-extended queries" \
  "Fix the CodeQL findings above, then rerun." bash scripts/codeql-security

echo "==> Fuzzing runs separately, because bash scripts/fuzz needs Linux x86_64 or XQUIK_FUZZ_HOST."
current_check="Final tree check"
if [[ -n "$status_commit" ]]; then
  [[ "$(git rev-parse HEAD)" == "$status_commit" && -z "$(git status --porcelain)" ]] || {
    echo "check-local: the tree changed during the checks. Commit or remove the changes, then rerun." >&2
    exit 1
  }
fi
printf -v joined '%s, ' "${passed[@]}"
summary="${joined%, }."
echo "All local checks passed. They were $summary"
