#!/usr/bin/env bash

# Snapshots of the settings store.
#
# The restore path overwrites the file the whole desktop reads, so the
# properties that matter are: a corrupt snapshot is never restored over a
# working configuration, a restore snapshots what it replaces so it is itself
# undoable, and a snapshot name cannot be used to reach a file outside the
# backup directory.

set -euo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-settings-backup"
work="$(mktemp -d /tmp/panama-backup-contract.XXXXXX)"

fail() {
    printf 'settings backup contract: %s\n' "$1" >&2
    exit 1
}
cleanup() { rm -rf "$work"; }
trap cleanup EXIT

settings="$work/config/panama/settings.json"
home="$work/state/panama/panama-home.json"
backups="$work/state/panama/backups"
transaction_dir="$work/state/panama/transactions/settings-restore"
mkdir -p "$(dirname "$settings")"

run() { XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" "$helper" "$@"; }
run_with() { XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" env "$@"; }

assert_transaction_clean() {
    if [[ -d "$transaction_dir" ]] && find "$transaction_dir" -mindepth 1 -print -quit | rg -q .; then
        fail 'restore left staged, rollback, or journal files behind'
    fi
    if find "$work" -type f \( \
            -name '.settings-restore.*' -o -name '.home-restore.*' \
            -o -name '*rollback*' -o -name '.journal.json.*' \
            -o -name '.settings.json.*' -o -name '.panama-home.json.*' \
        \) -print -quit | rg -q .; then
        fail 'restore left a temporary target or journal file behind'
    fi
}

# ── Nothing to back up ───────────────────────────────────────────────────────
run save >/dev/null 2>&1 && fail 'backing up a missing settings file reported success'
[[ "$(run list)" == "[]" ]] || fail 'an empty backup directory did not list as empty'

# ── A snapshot round-trips ───────────────────────────────────────────────────
printf '{"gapsOut":24,"windowRounding":6,"displays":{"DP-2":{"mode":"3840x2160@60","scale":2,"transform":0}}}' >"$settings"
mkdir -p "$(dirname "$home")"
printf '{"initialized":true,"favorites":[{"id":"light.desk","alias":"Desk"}]}' >"$home"
run save >/dev/null || fail 'save failed on a valid settings file'
name="$(run list | jq -r '.[0].name')"
[[ "$name" =~ ^settings-[0-9]{8}-[0-9]{9}\.json$ ]] || fail "unexpected snapshot name: $name"
[[ "$(run list | jq -r '.[0].keys')" == "3" ]] || fail 'snapshot key count is wrong'

printf '{"gapsOut":99,"displays":{"DP-2":{"mode":"4500x3000@60","scale":1.5,"transform":0}}}' >"$settings"
printf '{"initialized":false,"favorites":[]}' >"$home"
restore_result="$(run restore "$name")" || fail 'restore failed'
[[ "$(jq -r .gapsOut "$settings")" == "24" ]] || fail 'restore did not bring back the snapshot contents'
[[ "$(jq -r .windowRounding "$settings")" == "6" ]] || fail 'restore lost a key'
[[ "$(jq -r '.displays["DP-2"].scale' "$settings")" == "1.5" ]] \
    || fail 'restore bypassed display confirmation by applying snapshot geometry'
[[ "$(jq -r '.favorites[0].id' "$home")" == "light.desk" ]] || fail 'restore did not bring back Home favourites'
[[ "$(jq -r '.favorites[0].alias' "$home")" == "Desk" ]] || fail 'restore lost a Home alias'
jq -e '.home.present == true and .home.data.favorites[0].id == "light.desk"' <<<"$restore_result" >/dev/null \
    || fail 'restore did not return Home state for the live service to reload'

# ── Absence is part of a snapshot ───────────────────────────────────────────
rm -f "$home"
printf '{"gapsOut":30}' >"$settings"
run save >/dev/null || fail 'save failed when Home state was absent'
absent_name="$(run list | jq -r '.[0].name')"
printf '{"initialized":true,"favorites":[{"id":"light.living_room","alias":"Living room"}]}' >"$home"
absent_result="$(run restore "$absent_name")" || fail 'restore failed for a snapshot without Home state'
[[ ! -e "$home" ]] || fail 'restore did not preserve the snapshot’s absent Home state'
jq -e '.home.present == false and (.home | has("data") | not)' <<<"$absent_result" >/dev/null \
    || fail 'restore did not return absent Home state for the live service to reload'

# Desktop absence is symmetric for ordinary preferences, but confirmed display
# geometry is protected state: it must survive even a Home-only snapshot.
rm -f "$settings"
printf '{"initialized":true,"favorites":[{"id":"light.porch","alias":"Porch"}]}' >"$home"
run save >/dev/null || fail 'save failed when desktop settings were absent'
desktop_absent_name="$(run list | jq -r '.[0].name')"
printf '{"gapsOut":47,"windowRounding":9,"displays":{"DP-2":{"mode":"4500x3000@60","scale":1.5,"transform":0}}}' >"$settings"
printf '{"initialized":false,"favorites":[]}' >"$home"
run restore "$desktop_absent_name" >/dev/null || fail 'Home-only snapshot restore failed'
[[ -e "$settings" ]] || fail 'Home-only restore discarded confirmed display geometry'
[[ "$(jq -r '.displays["DP-2"].scale' "$settings")" == "1.5" ]] \
    || fail 'Home-only restore changed confirmed display geometry'
[[ "$(jq 'keys == ["displays"]' "$settings")" == "true" ]] \
    || fail 'Home-only restore retained ordinary desktop preferences'
[[ "$(jq -r '.favorites[0].id' "$home")" == "light.porch" ]] \
    || fail 'Home-only snapshot did not restore Home state'
assert_transaction_clean

printf '{"gapsOut":17}' >"$settings"

# A legacy settings-only snapshot predates presence metadata. Its safest
# interpretation is to restore desktop settings without deleting current Home
# state that the old format knew nothing about.
legacy="settings-20000101-010203004.json"
printf '{"gapsOut":17}' >"$backups/$legacy"
printf '{"initialized":true,"favorites":[{"id":"light.office","alias":"Office"}]}' >"$home"
run restore "$legacy" >/dev/null || fail 'legacy snapshot restore failed'
[[ "$(jq -r .gapsOut "$settings")" == "17" ]] || fail 'legacy snapshot did not restore desktop settings'
[[ "$(jq -r '.favorites[0].id' "$home")" == "light.office" ]] || fail 'legacy snapshot destroyed Home state it did not describe'

# `version` is a valid unknown desktop preference. It is only an envelope when
# the complete v2 shape is present.
legacy_version="settings-20000101-010203005.json"
printf '{"version":77,"gapsOut":19}' >"$backups/$legacy_version"
run restore "$legacy_version" >/dev/null || fail 'a legacy snapshot with an unknown version key was rejected'
[[ "$(jq -r '.version' "$settings")" == "77" ]] || fail 'legacy version key was not restored as desktop data'
[[ "$(jq -r '.favorites[0].id' "$home")" == "light.office" ]] || fail 'legacy version key changed Home state'

# ── A durable journal recovers a process/power-loss split ────────────────────
printf '{"gapsOut":28,"windowRounding":12}' >"$settings"
printf '{"initialized":true,"favorites":[{"id":"light.desk","alias":"Snapshot"}]}' >"$home"
run save >/dev/null || fail 'could not create crash-recovery snapshot'
crash_name="$(run list | jq -r '.[0].name')"

printf '{"gapsOut":91,"windowRounding":3}' >"$settings"
printf '{"initialized":true,"favorites":[{"id":"light.office","alias":"Before crash"}]}' >"$home"
run_with PANAMA_SETTINGS_BACKUP_TEST_CRASH=after-desktop "$helper" restore "$crash_name" >/dev/null 2>&1 \
    && fail 'crash injection completed restore instead of terminating after the first replacement'
[[ "$(jq -r '.gapsOut' "$settings")" == "28" ]] || fail 'crash did not occur after desktop replacement'
[[ "$(jq -r '.favorites[0].alias' "$home")" == "Before crash" ]] || fail 'crash unexpectedly replaced Home state'
[[ -f "$transaction_dir/journal.json" ]] || fail 'crash left no durable recovery journal'

# Every entry point must recover before doing its own work. `list` is the least
# invasive proof and must put both stores back to the pre-restore generation.
run list >/dev/null || fail 'next invocation could not recover the interrupted restore'
[[ "$(jq -r '.gapsOut' "$settings")" == "91" ]] || fail 'recovery did not roll desktop settings back'
[[ "$(jq -r '.favorites[0].alias' "$home")" == "Before crash" ]] || fail 'recovery did not keep Home state in the same generation'
assert_transaction_clean

# Cleanup is installed before staging. A deterministic pre-journal failure
# must leave both destinations untouched and no hidden artifacts behind.
run_with PANAMA_SETTINGS_BACKUP_TEST_FAIL=after-desktop-stage "$helper" restore "$crash_name" >/dev/null 2>&1 \
    && fail 'staging failure injection unexpectedly restored the snapshot'
[[ "$(jq -r '.gapsOut' "$settings")" == "91" ]] || fail 'staging failure changed desktop settings'
[[ "$(jq -r '.favorites[0].alias' "$home")" == "Before crash" ]] || fail 'staging failure changed Home state'
assert_transaction_clean

# ── Restoring snapshots what it replaced, so it is undoable ──────────────────
count="$(run list | jq 'length')"
[[ "$count" -ge 2 ]] || fail "restore did not snapshot the replaced settings (only $count snapshots)"

# ── A corrupt snapshot is refused ────────────────────────────────────────────
bad="settings-19990101-000000000.json"
mkdir -p "$backups"
printf '{ truncated' >"$backups/$bad"
run restore "$bad" >/dev/null 2>&1 && fail 'a corrupt snapshot was restored'
[[ "$(jq -r .gapsOut "$settings")" == "91" ]] || fail 'a refused restore still damaged the settings file'

invalid_home="settings-19990101-000000001.json"
jq -n '{
    version: 2,
    desktop: {present: true, data: {gapsOut: 88}},
    home: {present: true, data: {
        initialized: true,
        favorites: [
            {id: "light.desk", alias: "Desk"},
            {id: "light.desk", alias: "Duplicate"}
        ]
    }}
}' >"$backups/$invalid_home"
run restore "$invalid_home" >/dev/null 2>&1 && fail 'a snapshot with duplicate Home favourites was restored'
[[ "$(jq -r .gapsOut "$settings")" == "91" ]] || fail 'an invalid Home snapshot still damaged desktop settings'

printf '{ truncated' >"$home"
run save >/dev/null 2>&1 && fail 'a corrupt Home state file was backed up'
printf '{"initialized":true,"favorites":[]}' >"$home"

# ── The live service can sync its private Home state before save ─────────────
rm -f "$home"
printf '{"gapsOut":21}' >"$settings"
live_home='{"initialized":true,"favorites":[{"id":"light.studio","alias":"Studio"}]}'
run save "$live_home" >/dev/null || fail 'save rejected valid live Home state'
live_name="$(run list | jq -r '.[0].name')"
jq -e '.home.present == true and .home.data.favorites[0].alias == "Studio"' \
    "$backups/$live_name" >/dev/null \
    || fail 'live Home state was not written to the canonical snapshot'

# ── A snapshot cannot name a path outside the backup directory ───────────────
printf '{"pwned":true}' >"$work/outside.json"
run restore "../../outside.json" >/dev/null 2>&1 && fail 'a traversing snapshot name was accepted'
run restore "/etc/passwd" >/dev/null 2>&1 && fail 'an absolute snapshot path was accepted'
link_name="settings-20000101-000000001.json"
ln -s "$work/outside.json" "$backups/$link_name"
run restore "$link_name" >/dev/null 2>&1 && fail 'a snapshot symlink escaping the backup directory was accepted'
jq -e 'has("pwned") | not' "$settings" >/dev/null || fail 'a file outside the backup directory was restored'

# ── A snapshot that is not listed is refused ─────────────────────────────────
run restore "settings-20000101-000000000.json" >/dev/null 2>&1 && fail 'a missing snapshot was reported restored'

# ── Snapshots are capped ─────────────────────────────────────────────────────
for _ in $(seq 1 20); do
    printf '{"n":%s}' "$RANDOM" >"$settings"
    run save >/dev/null
done
kept="$(run list | jq 'length')"
[[ "$kept" -le 15 ]] || fail "snapshots are not capped: $kept kept"
[[ "$kept" -ge 10 ]] || fail "snapshot pruning was too aggressive: only $kept kept"

trap - EXIT
cleanup
printf 'settings backup contract: PASS\n'
