#!/bin/bash
# test/lint.sh
#
# Fast, offline integrity checks for the dotfiles repo.
# Does NOT require Docker or any packages to be installed beyond:
#   - shellcheck          (silently skipped if unavailable)
#   - bash                (for `bash -n` syntax checks)
#
# Run with:  make test-lint

set -uo pipefail

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

PASS=0
FAIL=0

say_fail() { printf 'FAIL: %s\n' "$*"; FAIL=$((FAIL+1)); }
say_pass() { printf 'ok:   %s\n' "$*"; PASS=$((PASS+1)); }

# ---------------------------------------------------------------------------
# 1. Collect scripts to check.
#    Only repo-owned scripts; skip vendored tpm/ plugin shell scripts.
#    Preferred source is git ls-files (the repo checkout); fall back to a
#    filesystem scan when .git is not present (e.g. inside the docker dev
#    image, where the repo is COPYied without .git). Note: git ls-files can
#    also silently return nothing when the mount has dubious ownership, so the
#    fallback must stay robust by excluding vendored plugin trees ('*/plugins/').
# ---------------------------------------------------------------------------
# Exclusions: never lint tpm or general vendored plugin trees, whose third-party
# scripts may legitimately fail `bash -n` and are not written/maintained here.
EXCLUDE_VENDOR='(/tpm/|/plugins/|/plugins$)'
mapfile -t SCRIPTS < <(git ls-files '*.sh' 2>/dev/null | grep -vE "$EXCLUDE_VENDOR")
if [[ ${#SCRIPTS[@]} -eq 0 ]]; then
    mapfile -t SCRIPTS < <(find . -name '*.sh' \
        -not -path './.git/*' -not -path '*/.git/*' \
        -not -path '*/tpm/*' -not -path '*/plugins/*' \
        | sort)
fi

if [[ ${#SCRIPTS[@]} -eq 0 ]]; then
    say_fail "no *.sh files found"
    exit 1
fi

echo "== Checking $((${#SCRIPTS[@]})) tracked shell scripts =="

# ---------------------------------------------------------------------------
# 2. Syntax check (bash -n) - always runs, no external deps.
# ---------------------------------------------------------------------------
for s in "${SCRIPTS[@]}"; do
    if bash -n "$s" 2>/dev/null; then
        :
    else
        say_fail "bash -n $s"
    fi
done
[[ $FAIL -eq 0 ]] && say_pass "bash -n syntax check passed"

# ---------------------------------------------------------------------------
# 3. shellcheck (optional dependency).
# ---------------------------------------------------------------------------
if command -v shellcheck >/dev/null 2>&1; then
    echo "== Running shellcheck =="
    if shellcheck --version >/dev/null 2>&1; then
        if shellcheck -S warning "${SCRIPTS[@]}" ; then
            say_pass "shellcheck (warning severity)"
        else
            say_fail "shellcheck reported issues (warning severity)"
        fi
    else
        say_fail "shellcheck binary present but not functional"
    fi
else
    echo "== shellcheck not installed; skipping (install with: pacman -S shellcheck) =="
fi

# ---------------------------------------------------------------------------
# 4. packages.txt parsing sanity (mirrors each setup.sh read_packages).
# ---------------------------------------------------------------------------
echo "== Checking packages.txt markers =="
for pkgdir in term basic utils; do
    if ! [[ -f "$pkgdir/packages.txt" ]]; then
        say_fail "$pkgdir/packages.txt missing"
        continue
    fi
    if grep -q '^@pacman' "$pkgdir/packages.txt"; then
        say_pass "$pkgdir/packages.txt declares @pacman section"
    else
        say_fail "$pkgdir/packages.txt missing @pacman marker"
    fi
    # every @aur/@npm marker must be matched by a real consumer in setup.sh
    for marker in '@aur' '@npm'; do
        if grep -q "^$marker" "$pkgdir/packages.txt"; then
            grep -q "$marker" "$pkgdir/setup.sh" \
                && say_pass "$pkgdir/packages.txt $marker handled by $pkgdir/setup.sh" \
                || say_fail "$pkgdir/packages.txt uses $marker but $pkgdir/setup.sh does not handle it"
        else
            say_pass "$pkgdir/packages.txt has no $marker section"
        fi
    done
done

# ---------------------------------------------------------------------------
# 5. Makefile <-> install script name consistency.
#    Catches typos such as `_install_skripte_install_basic_pcks.sh`
#    (missing '/') in Makefile targets.
# ---------------------------------------------------------------------------
echo "== Checking Makefile references install scripts that exist =="
# Pull out every `*_install_*_pcks.sh` token mentioned in the Makefile.
while IFS= read -r tok; do
    [[ -z "$tok" ]] && continue
    if [[ -e "$REPO_ROOT/$tok" ]]; then
        say_pass "Makefile references existing script: $tok"
    else
        say_fail "Makefile references missing script: $tok"
    fi
done < <(grep -P '^\t' Makefile | grep -oE '[_A-Za-z0-9/-]*install_[A-Za-z0-9_-]*\.sh' | sort -u)

# ---------------------------------------------------------------------------
# 6. Makefile <-> stow package name consistency.
#    Catches bugs such as `stow nvim` when the actual package dir is `term/`.
# ---------------------------------------------------------------------------
echo "== Checking Makefile stow targets reference real package dirs =="
# A 'package' is any tracked top-level directory (a .config/ subtree is stowable).
have_pkg() {
    local name="$1"
    if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
        git ls-files 2>/dev/null | awk -F/ 'NF>1 {print $1}' | sort -u | grep -qx "$name"
    else
        # git-less checkout (e.g. docker dev image): fall back to filesystem.
        # Only verify the dir exists; "is it stowable" is validated by stow
        # itself during the smoke test.
        [[ -d "$name" ]]
    fi
}

while IFS= read -r pkg; do
    [[ -z "$pkg" ]] && continue
    case "$pkg" in
        \#*|"") continue ;;
    esac
    if have_pkg "$pkg" && [[ -d "$REPO_ROOT/$pkg" ]]; then
        say_pass "Makefile stows existing package: $pkg"
    else
        say_fail "Makefile stows non-existent package: $pkg"
    fi
done < <(grep -P '^\t' Makefile | grep -oE 'stow [A-Za-z0-9_-]+' | awk '{print $2}' | sort -u)

# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo
echo "=============================================="
echo "lint.sh: $PASS passed, $FAIL failed"
echo "=============================================="
[[ $FAIL -eq 0 ]]
