#!/usr/bin/env bash

# The applications built from source, and the standard they have to meet.
#
# Every other application Panama installs comes from dnf or Flathub. These do
# not, and the rule for admitting one is not "it was convenient": there has to
# be no packaged form, the reason has to be written down, and what lands on the
# system still has to be a package the system owns.
#
# The rules:
#
#   1. Every definition declares a repository, a description, and a build.
#      A file missing any of them is an entry that fails only when somebody
#      asks for it, which is the worst moment to find out.
#   2. Every definition states why the exception exists. This is the whole
#      guard against the list growing by habit -- sunhat had seventy-odd
#      installers and no reason recorded for any of them.
#   3. Nothing is pinned. A recorded version is a 404 waiting to happen: every
#      pinned URL sunhat carried had rotted within a release cycle, which is the
#      argument this repository's package rule is built on.
#   4. `panama app` lists what the directory holds and refuses what it does not.
#
# Definitions are read, not run. Building one downloads an upstream release and
# installs a package, which is not something a test suite does.

set -uo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
apps_dir="$repo_dir/setup/apps"
panama="$repo_dir/bin/panama"

findings=()
note() { findings+=("$1"); }

[[ -d "$apps_dir" ]] || { printf 'apps contract: no %s\n' "$apps_dir" >&2; exit 1; }

shopt -s nullglob
definitions=("$apps_dir"/*)

# ── 1 & 2. Each definition is complete, and says why it exists ───────────────

for definition in "${definitions[@]}"; do
    name="$(basename "$definition")"
    [[ -f "$definition" ]] || { note "$name is not a file"; continue; }

    # Sourced in a subshell so one definition cannot leak into the next, and so
    # a definition that runs something at source time is contained.
    problems="$(
        description=""
        repo=""
        unset -f build 2>/dev/null || true
        # shellcheck source=/dev/null
        source "$definition" >/dev/null 2>&1

        [[ -n "$description" ]] || { printf 'no-description\n'; exit 0; }
        [[ -n "$repo" ]] || { printf 'no-repo\n'; exit 0; }
        declare -F build >/dev/null || { printf 'no-build\n'; exit 0; }
        [[ "$repo" == https://* ]] || { printf 'insecure-repo\n'; exit 0; }
    )"

    # Read back through a here-string rather than a pipe: a `while read` on the
    # right of a pipe runs in a subshell, and every finding it recorded was
    # being discarded at the end of the loop. Caught by standing in a broken
    # definition and watching two of the three checks stay silent.
    while read -r problem; do
        [[ -n "$problem" ]] || continue
        case "$problem" in
            no-description) note "$name has no description, so it cannot be listed" ;;
            no-repo)        note "$name declares no repository" ;;
            no-build)       note "$name declares no build function" ;;
            insecure-repo)  note "$name is cloned over something other than https" ;;
        esac
    done <<<"$problems"

    # The comment block is the reason. A definition without one is an entry
    # somebody added because it was easy.
    reason="$(grep -c '^#' "$definition")"
    (( reason >= 3 )) \
        || note "$name records no reason for being a source build rather than a package"

    # ── 3. Nothing pinned ───────────────────────────────────────────────────
    if grep -qE 'git (checkout|clone).*(-b|--branch|--tag)|checkout [0-9a-f]{7,40}|v[0-9]+\.[0-9]+\.[0-9]+' "$definition"; then
        note "$name looks like it pins a version or tag, which is what goes stale"
    fi
done

# ── 4. The command agrees with the directory ────────────────────────────────

listing="$("$panama" app 2>&1)"
for definition in "${definitions[@]}"; do
    [[ -f "$definition" ]] || continue
    grep -q "$(basename "$definition")" <<<"$listing" \
        || note "$(basename "$definition") is not listed by 'panama app'"
done

"$panama" app definitely-not-an-app >/dev/null 2>&1 \
    && note "'panama app' accepts a name that has no definition"

# The build tree belongs in the cache: it is entirely rebuildable, and a
# checkout kept beside the repository would eventually be mistaken for one.
grep -q 'XDG_CACHE_HOME' "$panama" \
    || note 'application checkouts are not placed under the cache directory'

# Not part of the unattended run, for the reason the interview exists.
grep -q 'app)' "$repo_dir/install" \
    && note 'the installer runs a source build, which cannot be walked away from'

# ── Report ───────────────────────────────────────────────────────────────────

if (( ${#findings[@]} > 0 )); then
    mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
    printf 'apps contract: %d finding(s)\n' "${#findings[@]}" >&2
    printf '  - %s\n' "${findings[@]}" >&2
    exit 1
fi

printf 'apps contract: PASS (%d applications)\n' "${#definitions[@]}"
