#!/usr/bin/env bash
# scripts/quality/coverage.sh
#
# Measure line/branch test coverage of code/include/ via gcov + lcov.
#
# Builds the test suite with `--coverage` (gcc) or `-fprofile-instr-generate
# -fcoverage-mapping` (clang), runs ctest, and emits:
#   * build-coverage/coverage.info        — lcov tracefile
#   * build-coverage/lcov-html/index.html — browseable HTML report
#   * stdout: per-file summary + grand total
#
# Local-only.  Not gated in CI yet; once a coverage threshold is agreed
# with the reviewer (e.g. 80 %), the gate can be a single line in
# cpp-tests.yml.
#
# Usage:
#   bash scripts/quality/coverage.sh              # gcc default
#   CXX=g++-13 bash scripts/quality/coverage.sh   # specific compiler
#
# Prerequisites: gcov + lcov (apt install lcov / brew install lcov)
#
# Exit codes:
#   0  coverage report generated; prints %
#   1  tests failed (no usable trace)
#   2  prerequisite missing

set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"

BUILD_DIR="build-coverage"

command -v lcov >/dev/null 2>&1 || {
    echo "FAIL: lcov not found in PATH.  Install via:" >&2
    echo "        macOS:  brew install lcov" >&2
    echo "        Linux:  sudo apt install lcov" >&2
    exit 2
}

# On macOS we prefer brew-installed LLVM clang++ because Apple Clang's
# GCov-compatible `--coverage` runtime is known to deadlock during
# static-initializer profiling on arm64 with template-heavy code
# (Eigen + CGAL); the LLVM build does not.  Override with `CXX=...`.
DEFAULT_CXX="g++"
if [ -z "${CXX:-}" ] && [ "$(uname -s)" = "Darwin" ] \
       && [ -x /opt/homebrew/opt/llvm/bin/clang++ ]; then
    DEFAULT_CXX="/opt/homebrew/opt/llvm/bin/clang++"
fi
CXX_BIN="${CXX:-$DEFAULT_CXX}"
command -v "$CXX_BIN" >/dev/null 2>&1 || { echo "FAIL: $CXX_BIN not found" >&2; exit 2; }

echo "========================================"
echo "  Coverage build  (gcov + lcov)"
echo "  CXX:    $CXX_BIN ($($CXX_BIN --version | head -1))"
echo "  build:  $BUILD_DIR"
echo "========================================"
echo

cmake -S code -B "$BUILD_DIR" \
    -DCMAKE_CXX_COMPILER="$CXX_BIN" \
    -DCMAKE_CXX_FLAGS="--coverage -O0 -g" \
    -DCMAKE_EXE_LINKER_FLAGS="--coverage" \
    -DCMAKE_BUILD_TYPE=Debug \
    -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST \
    -Wno-dev

nice -n 19 cmake --build "$BUILD_DIR" --target conformallab_tests \
    -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 2)"

# Run the non-CGAL test suite.  (CGAL tests under coverage instrumentation
# blow up to ~6 GB RAM during compilation; out-of-scope for this gate.)
cd "$BUILD_DIR"
if ! ctest -E "^cgal\." --output-on-failure; then
    cd "$ROOT"
    echo "FAIL: coverage build's test run did not complete cleanly." >&2
    exit 1
fi
cd "$ROOT"

# ── Capture trace ────────────────────────────────────────────────────────────
# lcov ≥ 2.0 became strict about "inconsistent" / "unsupported" / "negative"
# diagnostics from gcov data; the GTest sources reliably trigger
# "inconsistent" because of their preprocessor gymnastics, and brew clang's
# gcov shim is older than the function-end-line tracking lcov wants.
# These are noise we cannot fix in our source tree — suppress them.
LCOV_TOLERANT=(
    --ignore-errors inconsistent
    --ignore-errors unsupported
    --ignore-errors negative
    --ignore-errors empty
    --ignore-errors mismatch
    --rc lcov_branch_coverage=1
)

lcov --capture --directory "$BUILD_DIR" \
     --output-file "$BUILD_DIR/coverage.raw.info" \
     --no-external \
     "${LCOV_TOLERANT[@]}" \
     >/dev/null 2>&1 || true

# Restrict to code/include/  (our public API surface; ignore deps/tests).
lcov --extract "$BUILD_DIR/coverage.raw.info" \
     "*/code/include/*" \
     --output-file "$BUILD_DIR/coverage.info" \
     "${LCOV_TOLERANT[@]}" \
     >/dev/null 2>&1 || true

# ── HTML report ──────────────────────────────────────────────────────────────
genhtml --branch-coverage --legend \
        --output-directory "$BUILD_DIR/lcov-html" \
        "${LCOV_TOLERANT[@]}" \
        "$BUILD_DIR/coverage.info" >/dev/null 2>&1 || true

# ── Summary to stdout ────────────────────────────────────────────────────────
echo
echo "── Coverage summary (code/include/) ─────────────────────────"
if [ -s "$BUILD_DIR/coverage.info" ]; then
    lcov --summary "$BUILD_DIR/coverage.info" "${LCOV_TOLERANT[@]}" 2>/dev/null \
        | grep -E "lines\.\.\.\.|functions|branches" \
        | sed 's/^/    /'
    echo
    echo "HTML report:  $BUILD_DIR/lcov-html/index.html"
    echo "              open $BUILD_DIR/lcov-html/index.html"
else
    echo "    WARN: coverage.info is empty — likely an lcov/gcov version"
    echo "          mismatch.  Tests passed; raw .gcda files are in $BUILD_DIR."
    echo "          Inspect with:  find $BUILD_DIR -name '*.gcda' | head"
fi
