#!/usr/bin/env bash
# `[bootstrap.linux.systemd.units]` entries can be declared absent, and
# `mise bootstrap unapply` removes the units a module wrote, timers included.
# A fake `systemctl` stands in for the user manager and keeps unit state in
# files so the test can see what was stopped, disabled, and removed.

if [[ "$(uname)" != "Linux" ]]; then
  exit 0
fi

mkdir -p fakebin runtime/systemd/private fake-systemd/active fake-systemd/enabled fake-systemd/edit-on-check fake-systemd/break-on-check
export XDG_RUNTIME_DIR="$PWD/runtime"
export PATH="$PWD/fakebin:$PATH"
STATE="$PWD/fake-systemd"
UNITS="$HOME/.config/systemd/user"

cat <<SH >fakebin/systemctl
#!/usr/bin/env bash
[[ \$1 == --user ]] && shift
echo "\$*" >>"$STATE/log"
case "\$1" in
is-active)
  [[ -e "$STATE/active/\$3" ]]
  exit \$?
  ;;
is-enabled)
  # Stand in for an edit made while the unapply prompt is open: the plan has
  # already read the unit file by the time it asks whether it is enabled.
  if [[ -e "$STATE/edit-on-check/\$2" ]]; then
    rm "$STATE/edit-on-check/\$2"
    echo "# edited while waiting" >>"$UNITS/\$2"
  fi
  # Or one that becomes unreadable: a directory in place of the unit file.
  if [[ -e "$STATE/break-on-check/\$2" ]]; then
    rm "$STATE/break-on-check/\$2" "$UNITS/\$2"
    mkdir "$UNITS/\$2"
  fi
  if [[ -e "$STATE/enabled/\$2" ]]; then
    echo enabled
  else
    echo disabled
    exit 1
  fi
  ;;
enable) touch "$STATE/enabled/\$2" ;;
disable) rm -f "$STATE/enabled/\$2" ;;
restart) touch "$STATE/active/\$2" ;;
stop) rm -f "$STATE/active/\$2" ;;
esac
exit 0
SH
chmod +x fakebin/systemctl

cat <<'TOML' >mise.toml
[bootstrap.linux.systemd.units.sync]
exec_start = "/bin/true"
type = "oneshot"
start = false
wanted_by = []

[bootstrap.linux.systemd.units.sync-timer]
on_boot_sec = "5min"
unit = "sync"
TOML
assert_succeed "mise bootstrap linux systemd-units apply --yes"
assert_succeed "test -f $UNITS/dev.mise.sync.service"
assert_succeed "test -f $UNITS/dev.mise.sync-timer.timer"
assert_succeed "test -e $STATE/active/dev.mise.sync-timer.timer"
assert_succeed "test -e $STATE/enabled/dev.mise.sync-timer.timer"
assert_contains "mise bootstrap linux systemd-units status" "active"
assert_contains "mise bootstrap linux systemd-units status" "inactive"

# `state = "absent"` needs no other key and removes whichever unit is installed
# for the name, so the timer does not have to be described again.
cat <<'TOML' >mise.toml
[bootstrap.linux.systemd.units.sync]
state = "absent"

[bootstrap.linux.systemd.units.sync-timer]
state = "absent"
TOML
assert_not_contains "mise bootstrap linux systemd-units status 2>&1" "unknown field"
assert_contains "mise bootstrap linux systemd-units status" "present"
assert_fail "mise bootstrap linux systemd-units status --missing"
# The bare declaration reports the timer that is installed, not the service
# its missing timer keys would default to.
assert_contains "mise bootstrap linux systemd-units status --json" '"unit": "dev.mise.sync-timer.timer"'
assert_not_contains "mise bootstrap linux systemd-units status --json" "dev.mise.sync-timer.service"
assert_contains "mise bootstrap status --json" '"unit": "dev.mise.sync-timer.timer"'
assert_not_contains "mise bootstrap status --json" "dev.mise.sync-timer.service"
cat <<'TOML' >mise.backup.toml
[bootstrap.linux.systemd.units.sync-timer]
state = "absent"
TOML
assert_contains "mise -v bootstrap unapply backup --dry-run 2>&1" "systemd-unit dev.mise.sync-timer.timer: keeping it"
assert_not_contains "mise -v bootstrap unapply backup --dry-run 2>&1" "dev.mise.sync-timer.service"
rm mise.backup.toml
assert_contains "mise bootstrap linux systemd-units apply --dry-run" "rm $UNITS/dev.mise.sync-timer.timer"
assert_succeed "test -f $UNITS/dev.mise.sync-timer.timer"
rm -f "$STATE/log"
assert_succeed "mise bootstrap linux systemd-units apply --yes"
assert_fail "test -e $UNITS/dev.mise.sync.service"
assert_fail "test -e $UNITS/dev.mise.sync-timer.timer"
assert_succeed "grep -qx 'stop dev.mise.sync-timer.timer' $STATE/log"
assert_succeed "grep -qx 'disable dev.mise.sync-timer.timer' $STATE/log"
assert_fail "grep -q 'dev.mise.sync-timer.service' $STATE/log"
assert_fail "test -e $STATE/active/dev.mise.sync-timer.timer"
assert_fail "test -e $STATE/enabled/dev.mise.sync-timer.timer"
# The timer stops before the service it triggers, and the manager reloads once
# the files are gone.
assert "grep -v '^is-' $STATE/log | head -n 1" "stop dev.mise.sync-timer.timer"
assert "tail -n 1 $STATE/log" "daemon-reload"
assert_contains "mise bootstrap linux systemd-units status" "absent"
assert_succeed "mise bootstrap linux systemd-units status --missing"
# Converged: a second apply has nothing to do.
rm -f "$STATE/log"
assert_succeed "mise bootstrap linux systemd-units apply --yes"
assert_fail "test -e $STATE/log"

# With both a timer and a service installed under the name, status lists each
# and apply stops, disables, and removes each once.
echo "[Timer]" >"$UNITS/dev.mise.sync-timer.timer"
echo "[Service]" >"$UNITS/dev.mise.sync-timer.service"
assert_contains "mise bootstrap linux systemd-units status --json" '"unit": "dev.mise.sync-timer.timer"'
assert_contains "mise bootstrap linux systemd-units status --json" '"unit": "dev.mise.sync-timer.service"'
assert "mise bootstrap linux systemd-units apply --dry-run | grep -c '^rm '" "2"
rm -f "$STATE/log"
assert_succeed "mise bootstrap linux systemd-units apply --yes"
assert_fail "test -e $UNITS/dev.mise.sync-timer.timer"
assert_fail "test -e $UNITS/dev.mise.sync-timer.service"
assert "grep -v '^is-' $STATE/log" "stop dev.mise.sync-timer.timer
disable dev.mise.sync-timer.timer
stop dev.mise.sync-timer.service
disable dev.mise.sync-timer.service
daemon-reload"

# A unit link whose target is gone still counts as installed, and is removed.
ln -s "$PWD/gone.service" "$UNITS/dev.mise.sync.service"
assert_fail "mise bootstrap linux systemd-units status --missing"
assert_succeed "mise bootstrap linux systemd-units apply --yes"
assert_fail "test -L $UNITS/dev.mise.sync.service"
assert_succeed "mise bootstrap linux systemd-units status --missing"

# `mise bootstrap unapply` removes the units a module wrote and keeps the ones
# the rest of the configuration still declares.
cat <<'TOML' >mise.toml
[bootstrap.linux.systemd.units.base]
exec_start = "/bin/true"
TOML
cat <<'TOML' >mise.backup.toml
[bootstrap.linux.systemd.units.base]
exec_start = "/bin/true"

[bootstrap.linux.systemd.units.backup]
exec_start = "/bin/true"
type = "oneshot"
start = false
wanted_by = []

[bootstrap.linux.systemd.units.backup-timer]
on_calendar = "daily"
unit = "backup"
TOML
assert_succeed "mise -E backup bootstrap linux systemd-units apply --yes"
assert_succeed "test -f $UNITS/dev.mise.backup-timer.timer"
assert_contains "mise bootstrap unapply backup --dry-run" "systemd-unit:dev.mise.backup-timer.timer"
assert_contains "mise bootstrap unapply backup --dry-run" "systemd-unit:dev.mise.backup.service"
assert_not_contains "mise bootstrap unapply backup --dry-run" "dev.mise.base.service"
assert_succeed "test -f $UNITS/dev.mise.backup-timer.timer"

# A unit edited after the plan, while the prompt is open, is kept too.
touch "$STATE/edit-on-check/dev.mise.backup.service"
assert_contains "mise bootstrap unapply backup --yes 2>&1" "changed since the plan; use --force to remove it"
assert_fail "test -e $STATE/edit-on-check/dev.mise.backup.service"
assert_fail "test -e $UNITS/dev.mise.backup-timer.timer"
assert_succeed "test -f $UNITS/dev.mise.backup.service"
assert_succeed "mise -E backup bootstrap linux systemd-units apply --yes"

# A unit that cannot be inspected after the plan is kept, and the other
# planned units are still removed.
touch "$STATE/break-on-check/dev.mise.backup.service"
assert_contains "mise bootstrap unapply backup --yes 2>&1" "systemd unit dev.mise.backup.service: kept"
assert_fail "test -e $STATE/break-on-check/dev.mise.backup.service"
assert_fail "test -e $UNITS/dev.mise.backup-timer.timer"
assert_succeed "test -d $UNITS/dev.mise.backup.service"
rmdir "$UNITS/dev.mise.backup.service"
assert_succeed "mise -E backup bootstrap linux systemd-units apply --yes"

# A unit edited since it was written is kept unless --force is given.
echo "# edited by hand" >>"$UNITS/dev.mise.backup.service"
assert_contains "mise bootstrap unapply backup --yes 2>&1" "use --force to remove it"
assert_fail "test -e $UNITS/dev.mise.backup-timer.timer"
assert_succeed "test -f $UNITS/dev.mise.backup.service"
assert_succeed "mise bootstrap unapply backup --force --yes"
assert_fail "test -e $UNITS/dev.mise.backup.service"
assert_succeed "test -f $UNITS/dev.mise.base.service"

# A declared unit with the other kind also installed under its name is kept,
# with both files, even when the declared unit is edited while the plan runs;
# the module's other units are still removed.
assert_succeed "mise -E backup bootstrap linux systemd-units apply --yes"
echo "[Service]" >"$UNITS/dev.mise.backup-timer.service"
touch "$STATE/edit-on-check/dev.mise.backup-timer.timer"
assert_contains "mise bootstrap unapply backup --yes 2>&1" "systemd-unit dev.mise.backup-timer.timer: keeping it, installed, differs; use --force to remove it"
assert_fail "test -e $STATE/edit-on-check/dev.mise.backup-timer.timer"
assert_succeed "test -f $UNITS/dev.mise.backup-timer.timer"
assert_succeed "test -f $UNITS/dev.mise.backup-timer.service"
assert_fail "test -e $UNITS/dev.mise.backup.service"
