#!/usr/bin/env bash

# Getting a file back must never lose the file that was already there, and
# nothing here may touch how the machine boots.
#
# Four rules:
#
#   1. Restore sets the current version aside instead of overwriting it. A
#      restore that destroys what you were about to compare against is how
#      someone loses the work they were trying to save.
#   2. No rollback. snapper's rollback changes the btrfs default subvolume, and
#      this system's fstab pins subvol= explicitly, which overrides it -- so a
#      rollback would report success and change nothing after a reboot. A
#      recovery feature that silently does nothing is worse than none.
#   3. Paths cannot escape the snapshot they came from.
#   4. Snapshot 0 is the live filesystem, not a snapshot, and can never be a
#      target for reading or deleting.
#
# Read-only: it reads snapshot state and exercises refusals. It never creates,
# deletes, or restores anything.

set -uo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-snapshots"
service="$repo_dir/config/dot/quickshell/services/Snapshots.qml"
page="$repo_dir/config/dot/quickshell/modules/settings/SnapshotsPage.qml"

fail() {
    printf 'snapshots contract: %s\n' "$1" >&2
    exit 1
}

for path in "$helper" "$service" "$page"; do
    [[ -r "$path" ]] || fail "missing $path"
done
[[ -x "$helper" ]] || fail 'panama-snapshots is not executable'

# ── 1. Restore keeps what was there ─────────────────────────────────────────
restore_body="$(sed -n '/^def restore/,/^def /p' "$helper")"
[[ -n "$restore_body" ]] || fail 'restore is missing'
grep -q 'before-restore' <<<"$restore_body" \
    || fail 'restore does not set the current version aside'
grep -q 'os.rename(destination, kept)' <<<"$restore_body" \
    || fail 'the current version is not moved before the snapshot copy is written'
# The move must happen BEFORE the copy, or there is nothing left to move.
rename_line="$(grep -n 'os.rename(destination, kept)' <<<"$restore_body" | head -1 | cut -d: -f1)"
copy_line="$(grep -n 'shutil.copy' <<<"$restore_body" | head -1 | cut -d: -f1)"
[[ -n "$rename_line" && -n "$copy_line" && "$rename_line" -lt "$copy_line" ]] \
    || fail 'the snapshot copy is written before the current version is set aside'

# ── 2. No rollback ──────────────────────────────────────────────────────────
grep -qE '"rollback"|set-default|btrfs subvolume set-default|undochange' "$helper" \
    && fail 'the helper reaches for rollback, which this system fstab would silently ignore'
grep -qiE 'rollback' "$(dirname "$page")/$(basename "$page")" \
    | grep -v '^\s*//' >/dev/null 2>&1
page_code="$(grep -vE '^\s*//' "$page")"
grep -qi 'rollback' <<<"$page_code" \
    && fail 'the page offers rollback'

# ── 3. Paths cannot escape ──────────────────────────────────────────────────
grep -q 'def safe_relative' "$helper" || fail 'there is no path containment check'
command -v jq >/dev/null 2>&1 || { printf 'snapshots contract: SKIP (no jq)\n'; exit 0; }

snapshot="$("$helper" snapshot 2>/dev/null)" || fail 'snapshot failed'
config="$(jq -r '.configs[0].name // ""' <<<"$snapshot")"
number="$(jq -r '.configs[0].snapshots[0].number // 0' <<<"$snapshot")"

if [[ -n "$config" && "$number" != "0" ]]; then
    refusal() { "$helper" "$@" 2>/dev/null | jq -r '.error // ""'; }
    for bad in "../../etc" "../.." "gib/../../../etc"; do
        answer="$(refusal browse "$config" "$number" "$bad")"
        [[ "$answer" == "That path is not inside the snapshot." ]] \
            || fail "browsing \"$bad\" was not refused by the containment check: $answer"
    done
    answer="$(refusal restore "$config" "$number" "../../etc/passwd")"
    [[ "$answer" == "That path is not inside the snapshot." ]] \
        || fail "restoring \"../../etc/passwd\" was not refused: $answer"

    # ── 4. The live filesystem is not a snapshot ────────────────────────────
    # The REASON again: with the guard removed, snapshot 0 fails anyway because
    # its directory does not exist -- so a test that accepts any error passes
    # with the guard deleted and proves nothing.
    for answer in "$(refusal browse "$config" 0 "")" "$(refusal delete "$config" 0)"; do
        [[ "$answer" == "That is the current state, not a snapshot." ]] \
            || fail "snapshot 0 was rejected for the wrong reason, so the live filesystem is not actually guarded: $answer"
    done
fi

# ── Shape ───────────────────────────────────────────────────────────────────
jq -e '(.configs | type == "array") and (.unprotected | type == "array") and (.space | type == "object")' \
    <<<"$snapshot" >/dev/null || fail 'the snapshot is missing configs, unprotected, or space'
jq -e '[.configs[] | has("name") and has("subvolume") and has("timelineEnabled") and has("limits")] | all' \
    <<<"$snapshot" >/dev/null || fail 'a configuration is missing its name, subvolume, timeline flag, or limits'
jq -e '[.configs[].snapshots[]? | .number > 0] | all' <<<"$snapshot" >/dev/null \
    || fail 'the live filesystem is listed as a snapshot'

# ── Destructive actions are confirmed ───────────────────────────────────────
grep -q 'confirmingDelete' "$page" || fail 'the page deletes a snapshot without confirming'
grep -q 'confirmingRestore' "$page" || fail 'the page restores without confirming'
grep -q 'keeping whatever is there now' "$page" \
    || fail 'the page does not say that restoring keeps the current version'

# ── Space is reported honestly ──────────────────────────────────────────────
# Per-snapshot size needs btrfs quotas, which cost performance on every write.
# A made-up number would be worse than saying it is not measured.
grep -q 'Not measured' "$page" \
    || fail 'the page reports a per-snapshot size it cannot actually measure'

printf 'snapshots contract: PASS (%d volume(s), %d snapshot(s), no rollback)\n' \
    "$(jq '.configs | length' <<<"$snapshot")" \
    "$(jq '[.configs[].snapshots[]?] | length' <<<"$snapshot")"
