#!/usr/bin/env bash

# `panama update`: the routine command, and the promises that make it routine.
#
# An update that asks questions is an update nobody runs, and a machine nobody
# updates drifts until the next reinstall. So the properties below are the
# whole point of the command rather than details of it:
#
#   1. --upgrade NEVER runs the interview. This is the regression that started
#      the redesign: ./install asked the full first-install questionnaire every
#      time, including on a machine whose answers could not have changed.
#   2. --upgrade runs exactly the stages that need no answer, and never
#      setup-identity or install-hardware. Both act only on interview answers.
#   3. install-packages is gated on a content hash of the package lists, and
#      the hash is NOT recorded when the stage fails -- the same rule
#      panama-migrate applies to its markers, for the same reason.
#   4. A conflicted `git stash pop` leaves no conflict markers in the tree.
#      Every dotfile here is a symlink into the checkout, so a half-merged file
#      is not something to fix later: it is live in ~/.config immediately, and
#      a broken .qml costs the running shell.
#
# Driven against fixture stages in a throwaway PANAMA_PATH, with sudo and
# gsettings shimmed, so this never touches the machine running it.

set -uo pipefail

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

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

[[ -x "$installer" ]] || { printf 'update command contract: no installer at %s\n' "$installer" >&2; exit 1; }

tmp="$(mktemp -d -t panama-update-contract.XXXXXX)"
trap 'rm -rf "$tmp"' EXIT

STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings
             link-vicinae-scripts setup-server link-server setup-identity
             install-hardware)

# A PANAMA_PATH that looks enough like the real one for install to run, and
# records what it was asked to do instead of doing it.
build_fixture() {
    local root="$1" packages_rc="${2:-0}"
    rm -rf "$root"
    mkdir -p "$root/bin" "$root/setup/scripts" "$root/setup/packages" \
             "$root/config/dot/quickshell/scripts"

    cp "$installer" "$root/install"
    : >"$root/bin/ascii"
    printf 'base-package\n' >"$root/setup/packages/base"

    local stage
    for stage in "${STAGE_NAMES[@]}"; do
        cat >"$root/setup/scripts/$stage" <<EOF
#!/usr/bin/env bash
printf '%s\n' "$stage" >>"\$PANAMA_RAN"
EOF
        chmod +x "$root/setup/scripts/$stage"
    done
    # The one stage whose exit code the caller wants to control.
    cat >"$root/setup/scripts/install-packages" <<EOF
#!/usr/bin/env bash
printf 'install-packages\n' >>"\$PANAMA_RAN"
exit $packages_rc
EOF
    chmod +x "$root/setup/scripts/install-packages"

    cat >"$root/setup/scripts/interview" <<'EOF'
#!/usr/bin/env bash
printf 'interview\n' >>"$PANAMA_RAN"
: >"$PANAMA_ANSWERS"
EOF
    chmod +x "$root/setup/scripts/interview"

    cat >"$root/bin/panama-migrate" <<'EOF'
#!/usr/bin/env bash
printf 'migrate %s\n' "${1:-run}" >>"$PANAMA_RAN"
EOF
    chmod +x "$root/bin/panama-migrate"

    cat >"$root/config/dot/quickshell/scripts/panama-doctor" <<'EOF'
#!/usr/bin/env bash
printf 'doctor\n' >>"$PANAMA_RAN"
EOF
    chmod +x "$root/config/dot/quickshell/scripts/panama-doctor"

    # Nothing that reaches the real machine. sudo would prompt in a test run,
    # and gsettings would genuinely change the tester's screensaver.
    mkdir -p "$root/shim"
    cat >"$root/shim/sudo" <<'EOF'
#!/usr/bin/env bash
[[ "${1:-}" == -v || "${1:-}" == -n ]] && exit 0
exit 0
EOF
    cat >"$root/shim/gsettings" <<'EOF'
#!/usr/bin/env bash
exit 0
EOF
    cat >"$root/shim/hostnamectl" <<'EOF'
#!/usr/bin/env bash
exit 0
EOF
    chmod +x "$root/shim"/*
}

# Run the fixture installer and echo what ran, one stage per line.
run_install() {
    local root="$1"; shift
    local status=0
    : >"$root/ran"
    PATH="$root/shim:$PATH" PANAMA_PATH="$root" PANAMA_RAN="$root/ran" \
      XDG_STATE_HOME="$root/state" bash "$root/install" "$@" \
      >"$root/out" 2>&1 || status=$?
    cat "$root/ran"
    return "$status"
}

# ── 1. The interview never runs on an upgrade ────────────────────────────────

build_fixture "$tmp/a"
ran="$(run_install "$tmp/a" --upgrade)"

if grep -qx 'interview' <<<"$ran"; then
    note 'install --upgrade ran the interview, which is the whole regression this prevents'
fi

# And the control: a real install must still ask.
build_fixture "$tmp/b"
ran_install="$(run_install "$tmp/b")"
if ! grep -qx 'interview' <<<"$ran_install"; then
    note 'a plain ./install no longer asks the interview, so a new machine is never configured'
fi

# ── 2. Exactly the answer-free stages ────────────────────────────────────────

for stage in install-packages link-dotfiles link-skills link-user change-settings link-vicinae-scripts; do
    grep -qx "$stage" <<<"$ran" || note "install --upgrade did not run $stage"
done
for stage in setup-identity install-hardware; do
    grep -qx "$stage" <<<"$ran" \
        && note "install --upgrade ran $stage, which exists only to act on interview answers"
done

# A stage added to STAGES without a decision about which path owns it shows up
# here, because this list is written down twice on purpose.
declared=()
if declared_output="$(python3 - "$installer" <<'PY'
import re
import sys

found = False
for line in open(sys.argv[1], encoding="utf-8"):
    match = re.match(r'^\s*STAGES=\((.*)\)\s*$', line)
    if match and "$" not in match.group(1):
        found = True
        print("\n".join(re.findall(r"[\w-]+", match.group(1))))
if not found:
    print("install has no STAGES assignment", file=sys.stderr)
    sys.exit(1)
PY
)"; then
    while IFS= read -r stage; do
        [[ -n "$stage" ]] && declared+=("$stage")
    done <<<"$declared_output"
else
    note 'could not read the install STAGES assignment'
fi
for stage in "${declared[@]}"; do
    printf '%s\n' "${STAGE_NAMES[@]}" | grep -qx "$stage" \
        || note "install declares a stage this contract has never heard of: $stage"
done

# ── 3. The packages hash gates the stage, and a failure does not record it ───

# Second run, nothing changed: the stage must be skipped.
ran_again="$(run_install "$tmp/a" --upgrade)"
grep -qx 'install-packages' <<<"$ran_again" \
    && note 'install-packages ran again with the package lists unchanged'

# --packages overrides the hash.
ran_forced="$(run_install "$tmp/a" --upgrade --packages)"
grep -qx 'install-packages' <<<"$ran_forced" \
    || note '--packages did not force install-packages to run'

# A changed list brings the stage back.
printf 'another-package\n' >>"$tmp/a/setup/packages/base"
ran_changed="$(run_install "$tmp/a" --upgrade)"
grep -qx 'install-packages' <<<"$ran_changed" \
    || note 'a changed package list did not bring install-packages back'

# A failing stage must not record the hash, or the failure is hidden forever.
build_fixture "$tmp/c" 1
install_status=0
run_install "$tmp/c" --upgrade >/dev/null || install_status=$?
[[ "$install_status" -eq 1 ]] \
    || note "install --upgrade returned $install_status instead of the failing stage status"
if [[ -r "$tmp/c/state/panama/packages-hash" ]]; then
    note 'install-packages failed but its hash was recorded, so it will never be retried'
fi

# A full install always runs the stage, whatever any recorded hash says.
build_fixture "$tmp/d"
run_install "$tmp/d" --upgrade >/dev/null
ran_full="$(run_install "$tmp/d")"
grep -qx 'install-packages' <<<"$ran_full" \
    || note 'a full ./install skipped install-packages because of a recorded hash'

# ── 4. A conflicted pop never leaves markers in a live config ────────────────
#
# The fixture also covers a clean fast-forward, installer status propagation,
# and the boundary between update and sync before forcing the conflict below.

# Each fixture has the same three repositories as a real update: a bare remote,
# a clone that publishes upstream changes, and the machine clone being updated.
build_cli_fixture() (
    set -e
    local root="$1"
    rm -rf "$root"
    mkdir -p "$root"
    git init -q --bare "$root/origin.git"
    git clone -q "$root/origin.git" "$root/upstream" 2>/dev/null
    git -C "$root/upstream" config user.email contract@panama
    git -C "$root/upstream" config user.name contract

    mkdir -p "$root/upstream/bin"
    cp "$panama" "$root/upstream/bin/panama"
    cat >"$root/upstream/install" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"${PANAMA_UPDATE_FIXTURE_LOG:?}"
exit "${PANAMA_UPDATE_INSTALL_RC:-0}"
EOF
    chmod +x "$root/upstream/bin/panama" "$root/upstream/install"
    printf 'one\n' >"$root/upstream/f"
    git -C "$root/upstream" add -A
    git -C "$root/upstream" commit -qm initial
    git -C "$root/upstream" push -qu origin HEAD

    git clone -q "$root/origin.git" "$root/machine"
    git -C "$root/machine" config user.email contract@panama
    git -C "$root/machine" config user.name contract
)

advance_upstream() (
    set -e
    local root="$1" file="$2" contents="$3"
    printf '%s\n' "$contents" >"$root/upstream/$file"
    git -C "$root/upstream" add "$file"
    git -C "$root/upstream" commit -qm "update $file"
    git -C "$root/upstream" push -q
)

clean="$tmp/clean-update"
if build_cli_fixture "$clean" && advance_upstream "$clean" release new; then
    machine_before="$(git -C "$clean/machine" rev-parse HEAD)"
    upstream_after="$(git -C "$clean/upstream" rev-parse HEAD)"
    [[ "$machine_before" != "$upstream_after" ]] \
        || note 'the clean update fixture started current, so it cannot prove a fast-forward'

    : >"$clean/install.log"
    update_status=0
    PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
        "$clean/machine/bin/panama" update >"$clean/update.out" 2>&1 \
        || update_status=$?
    [[ "$update_status" -eq 0 ]] \
        || note "panama update failed on a clean clone with status $update_status"
    [[ "$(git -C "$clean/machine" rev-parse HEAD)" == "$upstream_after" ]] \
        || note 'panama update did not fast-forward the clean machine clone'
    grep -qx -- '--upgrade' "$clean/install.log" \
        || note 'panama update did not invoke the installer with --upgrade'

    : >"$clean/install.log"
    update_status=0
    PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" PANAMA_UPDATE_INSTALL_RC=23 \
        "$clean/machine/bin/panama" update >"$clean/failing-update.out" 2>&1 \
        || update_status=$?
    [[ "$update_status" -eq 23 ]] \
        || note "panama update returned $update_status instead of installer status 23"
    grep -qx -- '--upgrade' "$clean/install.log" \
        || note 'the failing update did not reach the fixture installer'

    : >"$clean/install.log"
    printf 'local sync\n' >"$clean/machine/synced"
    sync_status=0
    printf 'y\ncontract sync\n' \
        | PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
            "$clean/machine/bin/panama" sync >"$clean/sync.out" 2>&1 \
        || sync_status=$?
    [[ "$sync_status" -eq 0 ]] \
        || note "panama sync failed in the local fixture with status $sync_status"
    [[ ! -s "$clean/install.log" ]] \
        || note 'panama sync invoked the installer; sync and update are separate jobs'
else
    note 'the clean update fixture could not be built'
fi

# A conflicted stash pop must not leave markers in the checkout. The failed pop
# keeps the stash, so the local version remains recoverable after the reset.
conflict="$tmp/conflict-update"
if build_cli_fixture "$conflict"; then
    printf 'local\n' >"$conflict/machine/f"
    if advance_upstream "$conflict" f upstream; then
        : >"$conflict/install.log"
        conflict_status=0
        PANAMA_UPDATE_FIXTURE_LOG="$conflict/install.log" \
            "$conflict/machine/bin/panama" update >"$conflict/update.out" 2>&1 \
            || conflict_status=$?
        [[ "$conflict_status" -eq 0 ]] \
            || note "panama update failed while recovering a stash conflict with status $conflict_status"
        [[ "$(<"$conflict/machine/f")" == upstream ]] \
            || note 'panama update did not reset the conflicted file to the upstream version'
        if git -C "$conflict/machine" grep -qE '^(<<<<<<<|=======|>>>>>>>)' -- .; then
            note 'panama update left conflict markers in the machine checkout'
        fi
        [[ -n "$(git -C "$conflict/machine" stash list)" ]] \
            || note 'panama update dropped the stash after its conflicted pop'
        recovered="$(git -C "$conflict/machine" show 'stash@{0}:f' 2>/dev/null)"
        [[ "$recovered" == local ]] \
            || note 'the stash left by panama update does not contain the local version'
    else
        note 'the conflict fixture could not publish its upstream edit'
    fi
else
    note 'the conflict update fixture could not be built'
fi

if (( ${#findings[@]} > 0 )); then
    printf 'update command contract: %d finding(s)\n' "${#findings[@]}" >&2
    printf '  - %s\n' "${findings[@]}" >&2
    exit 1
fi

printf 'update command contract: PASS\n'
