#!/usr/bin/env bash
# `mise bootstrap dotfiles rollback` and `undo`: returning tracked files to the version a
# checkpoint holds, selectively, with the previous state saved first.
# shellcheck disable=SC2016

require_cmd git

count="mise bootstrap dotfiles history --json -n 0 | jq length"
config="$MISE_CONFIG_DIR/config.toml"

mkdir -p ~/.config/hypr "$MISE_CONFIG_DIR"
echo 'bind = SUPER, Q, exec, kitty' >~/.config/hypr/bindings.lua
echo 'monitor=DP-1' >~/.config/hypr/monitors.lua
cat <<EOF2 >"$config"
[history.reload]
"~/.config/hypr/**" = "echo reloaded >> $HOME/reloads"
EOF2
assert_succeed "mise bootstrap dotfiles track ~/.config/hypr"
baseline="$(mise bootstrap dotfiles history --json | jq -r '.[0].uuid')"

# noisy checkpoints of another file never influence which version a path returns to
for i in 1 2 3; do
  echo "state $i" >~/.config/hypr/state.json
  assert_succeed "mise bootstrap dotfiles save"
done
echo 'bind = SUPER, Q, exec, alacritty' >~/.config/hypr/bindings.lua
assert_succeed "mise bootstrap dotfiles save"
assert "$count" "5"

# path-first: the newest saved version that differs from the working tree
assert "mise bootstrap dotfiles rollback ~/.config/hypr/bindings.lua --dry-run | grep -c 'bindings.lua *write'" "1"
assert "cat ~/.config/hypr/bindings.lua" "bind = SUPER, Q, exec, alacritty"
assert "$count" "5"
echo 'monitor=DP-2' >~/.config/hypr/monitors.lua
assert_succeed "mise bootstrap dotfiles rollback ~/.config/hypr/bindings.lua --yes"
assert "cat ~/.config/hypr/bindings.lua" "bind = SUPER, Q, exec, kitty"
# unrelated later work is preserved, the reload hook ran once, and the pair is recorded
assert "cat ~/.config/hypr/monitors.lua" "monitor=DP-2"
assert "cat ~/reloads | wc -l | tr -d ' '" "1"
assert "mise bootstrap dotfiles history --json | jq -r '.[0].trigger, .[1].trigger'" $'rollback\nrollback-before'
assert "mise bootstrap dotfiles history --json | jq -r '.[0].operation | \"\\(.kind) \\(.status) \\(.affected | join(\",\"))\"'" "rollback completed ~/.config/hypr/bindings.lua"
assert "mise bootstrap dotfiles history --json | jq -r '.[0].operation.to == (.[] | select(.id == 4) | .uuid)'" "true"
assert_contains "mise bootstrap dotfiles history show latest" "rolled back ~/.config/hypr/bindings.lua to checkpoint 4"
# the protective checkpoint holds what was overwritten, including the unsaved edit
assert "mise bootstrap dotfiles history --json | jq -r '.[1].changes.modified | join(\",\")'" "~/.config/hypr/monitors.lua"

# undo reverses exactly the paths the rollback touched
assert_succeed "mise bootstrap dotfiles undo --yes"
assert "cat ~/.config/hypr/bindings.lua" "bind = SUPER, Q, exec, alacritty"
assert "cat ~/.config/hypr/monitors.lua" "monitor=DP-2"
assert "mise bootstrap dotfiles history --json | jq -r '.[0].operation | \"\\(.kind) \\(.undoes | length)\"'" "undo 40"
assert_contains "mise bootstrap dotfiles history show latest" "undid rollback"
# undo of an undo re-applies; nothing is left to undo after that but the undo itself
assert_succeed "mise bootstrap dotfiles undo --yes"
assert "cat ~/.config/hypr/bindings.lua" "bind = SUPER, Q, exec, kitty"

# checkpoint-first: --to with --all deletes files created later and preserves
# unrelated later edits outside the selection; a path the checkpoint never covered
# is skipped, never deleted
echo extra >~/.config/hypr/extra.conf
mkdir -p ~/.config/other
echo other >~/.config/other/file
assert_succeed "mise bootstrap dotfiles save"
assert "mise bootstrap dotfiles rollback --to $baseline --all --dry-run | grep -c 'extra.conf *delete'" "1"
assert "mise bootstrap dotfiles rollback --to $baseline --all --dry-run | grep -c 'state.json *delete'" "1"
assert_succeed "mise bootstrap dotfiles rollback --to $baseline --all --yes"
assert_fail "test -e ~/.config/hypr/extra.conf"
assert_fail "test -e ~/.config/hypr/state.json"
assert "cat ~/.config/hypr/monitors.lua" "monitor=DP-1"
assert "cat ~/.config/other/file" "other"
assert_fail "mise bootstrap dotfiles rollback ~/.config/other/file" "is not tracked"

# a type change is a conflict unless forced
rm ~/.config/hypr/monitors.lua
mkdir ~/.config/hypr/monitors.lua
assert_contains "mise bootstrap dotfiles rollback ~/.config/hypr/monitors.lua --dry-run 2>&1" "conflict: a file became a directory"
assert_fail "mise bootstrap dotfiles rollback ~/.config/hypr/monitors.lua --yes" "conflict"
assert_succeed "mise bootstrap dotfiles rollback ~/.config/hypr/monitors.lua --yes --force"
assert "cat ~/.config/hypr/monitors.lua" "monitor=DP-1"

# an undo that must recreate an empty directory where something history does not
# capture now stands is refused before anything is written
rm ~/.config/hypr/monitors.lua && mkdir -p ~/.config/hypr/monitors.lua/sub
echo inner >~/.config/hypr/monitors.lua/f
assert_succeed "mise bootstrap dotfiles save"
assert_succeed "mise bootstrap dotfiles rollback ~/.config/hypr/monitors.lua --yes --force"
assert "cat ~/.config/hypr/monitors.lua" "monitor=DP-1"
printf '\n[history]\nexclude = ["~/.config/hypr/monitors.lua/sub"]\n' >>"$config"
rm ~/.config/hypr/monitors.lua && mkdir -p ~/.config/hypr/monitors.lua ~/elsewhere
ln -s ~/elsewhere ~/.config/hypr/monitors.lua/sub
assert_contains "mise bootstrap dotfiles undo --dry-run 2>&1" "conflict: a symlink history does not capture stands where a directory was"
assert_fail "mise bootstrap dotfiles undo --yes" "conflict"
assert_fail "test -e ~/.config/hypr/monitors.lua/f"
assert "readlink ~/.config/hypr/monitors.lua/sub" "$HOME/elsewhere"
rm ~/.config/hypr/monitors.lua/sub
assert_succeed "mise bootstrap dotfiles undo --yes"
assert "cat ~/.config/hypr/monitors.lua/f" "inner"
assert "test -d ~/.config/hypr/monitors.lua/sub && echo dir" "dir"
rm -r ~/.config/hypr/monitors.lua && echo 'monitor=DP-1' >~/.config/hypr/monitors.lua
assert_succeed "mise bootstrap dotfiles save"

# a config file rolled back prints the bootstrap hint; declarations are never run
echo '[settings]' >"$MISE_CONFIG_DIR/extra.toml"
assert_succeed 'mise bootstrap dotfiles track ~/.config/mise/extra.toml'
assert_succeed "mise bootstrap dotfiles save"
rm "$MISE_CONFIG_DIR/extra.toml"
assert_contains "mise bootstrap dotfiles rollback ~/.config/mise/extra.toml --yes 2>&1" 'run `mise bootstrap --dry-run`'
assert "cat '$MISE_CONFIG_DIR/extra.toml'" "[settings]"

# a rollback refuses a checkpoint whose run did not finish, and a missing state
assert_fail "mise bootstrap dotfiles rollback ~/.config/hypr/bindings.lua --to 99" 'no history checkpoint matches "99"'
assert_fail "mise bootstrap dotfiles undo $baseline" "not an operation"

# no-op: rolling back a path that already matches records nothing
n="$(eval "$count")"
assert_contains "mise bootstrap dotfiles rollback ~/.config/hypr/bindings.lua --to latest --yes 2>&1" "nothing to do"
assert "$count" "$n"

# reload commands come from the trusted layers only
mkdir -p ~/proj
printf '[history.reload]\n"~/.config/hypr/**" = "echo project >> %s/reloads"\n' "$HOME" >~/proj/mise.toml
echo 'bind = X' >~/.config/hypr/bindings.lua
assert_succeed "mise bootstrap dotfiles save"
: >~/reloads
assert_succeed "cd ~/proj && mise trust -q && mise bootstrap dotfiles rollback ~/.config/hypr/bindings.lua --yes"
assert "cat ~/reloads" "reloaded"
