#!/usr/bin/env bash
# `mise installs migrate` reinstalls legacy installations (made before the
# identity install layout was turned on) into their own hashed directories, and
# leaves the version link where the old directory was.
export MISE_EXPERIMENTAL=1
installs="$MISE_DATA_DIR/installs"

echo "=== legacy installations, made with the layout off ==="
assert "mise install dummy@1.0.0 dummy@1.1.0 tiny@3.1.0"
for v in 1.0.0 1.1.0; do
  assert "test -d '$installs/dummy/$v' && test ! -L '$installs/dummy/$v'"
done
# something only the old copy has, to tell a reinstall from a move
echo old >"$installs/dummy/1.1.0/marker"

export MISE_INSTALL_LAYOUT=identity

echo "=== a dry run lists what would move and changes nothing ==="
assert_contains "mise installs migrate --dry-run" "would migrate dummy@1.1.0"
assert_contains "mise installs migrate --dry-run" "would migrate tiny@3.1.0"
assert "test ! -L '$installs/dummy/1.1.0' && test -f '$installs/dummy/1.1.0/marker'"
# a version names the installed versions it matches, as it does for uninstall
assert_contains "mise installs migrate --dry-run dummy@1.1" "would migrate dummy@1.1.0"
assert_not_contains "mise installs migrate --dry-run dummy@1.1" "dummy@1.0.0"
assert_contains "mise installs migrate --dry-run dummy@latest" "would migrate dummy@1.1.0"
assert_contains "mise installs migrate --dry-run dummy@1 2>&1" "would migrate dummy@1.0.0"
assert_not_contains "mise installs migrate --dry-run dummy@1.0 2>&1" "dummy@1.1.0"
assert_not_contains "mise installs migrate --dry-run dummy@latest" "dummy@1.0.0"

echo "=== one version moves; the version link takes its place ==="
assert_contains "mise installs migrate dummy@1.1.0" "migrated dummy@1.1.0"
new="$(mise where dummy@1.1.0)"
assert_matches "mise where dummy@1.1.0" "^$installs/dummy-[a-z2-7]{8}\$"
assert "test -f '$new/.mise-install.toml'"
assert "test ! -e '$new/marker'"
assert "readlink '$installs/dummy/1.1.0'" "../$(basename "$new")"
assert_contains "$installs/dummy/1.1.0/bin/dummy" "This is Dummy 1.1.0!"
# the runtime alias made for the legacy install still resolves, through the link
assert "readlink '$installs/dummy/1.1'" "./1.1.0"
assert_contains "$installs/dummy/1.1/bin/dummy" "This is Dummy 1.1.0!"
assert_empty "find '$installs/dummy' -maxdepth 1 -name '.*mise-migrating'"
# the version that was not named is still legacy
assert "test -d '$installs/dummy/1.0.0' && test ! -L '$installs/dummy/1.0.0'"

echo "=== a version that cannot be reinstalled is moved as it is, and is not an error ==="
# the dummy plugin refuses to install this version
mkdir -p "$installs/dummy/other-dummy/bin"
echo kept >"$installs/dummy/other-dummy/marker"
assert_contains "mise installs migrate dummy@other-dummy 2>&1" "relocated dummy@other-dummy"
legacy_moved="$(readlink "$installs/dummy/other-dummy")"
assert_matches "echo '$legacy_moved'" "^\.\./dummy-[a-z2-7]{8}\$"
# the same files, now an identity installation behind the version link
assert_contains "cat '$installs/dummy/other-dummy/marker'" "kept"
assert "test -f '$installs/$(basename "$legacy_moved")/.mise-install.toml'"
assert_matches "mise where dummy@other-dummy" "^$installs/dummy-[a-z2-7]{8}\$"
assert_contains "mise ls dummy 2>&1" "other-dummy"
assert_contains "mise installs migrate dummy@other-dummy 2>&1" "no legacy installations to migrate"
assert_empty "find '$installs/dummy' -maxdepth 1 -name '.*mise-migrating'"
assert_empty "ls '$installs/.mise/migrations'"
assert "mise uninstall dummy@other-dummy"
assert "test ! -e '$installs/dummy/other-dummy'"

echo "=== with no arguments, every remaining legacy installation moves ==="
# a version that cannot be reinstalled does not stop the others, or fail the run
mkdir -p "$installs/dummy/other-dummy/bin"
assert_contains "mise installs migrate 2>&1" "2 migrated, 1 relocated, 0 kept legacy, 0 failed"
assert "test -L '$installs/dummy/other-dummy'"
assert "mise uninstall dummy@other-dummy"
assert "test -L '$installs/dummy/1.0.0'"
assert "test -L '$installs/tiny/3.1.0'"
assert_matches "mise where tiny@3.1.0" "^$installs/tiny-[a-z2-7]{8}\$"
assert_contains "mise x tiny@3.1.0 -- rtx-tiny" "3.1.0"
assert_contains "mise installs migrate" "no legacy installations to migrate"
assert "mise installs ls --json | jq length" "3"

echo "=== an interrupted migration is finished or undone on the next run ==="
backend="$(mise installs ls dummy --json | jq -r 'first(.[].backend)')"
# what a run records before it moves the old directory aside
journal() { # <version> <finished> [installation directories its install wrote into...]
  local v="$1" finished="$2" made=""
  shift 2
  for dir in "$@"; do made="$made\"$dir\","; done
  mkdir -p "$installs/.mise/migrations"
  cat >"$installs/.mise/migrations/dummy@$v.toml" <<EOF
tool = "dummy"
legacy = "$installs/dummy/$v"
backend = "$backend"
version = "$v"
made = [${made%,}]
finished = $finished
EOF
}
assert "MISE_INSTALL_LAYOUT=legacy MISE_EXPERIMENTAL=0 mise install dummy@2.0.0"
assert "test -d '$installs/dummy/2.0.0' && test ! -L '$installs/dummy/2.0.0'"
# as if a run stopped right after moving the directory aside
journal 2.0.0 false
mv "$installs/dummy/2.0.0" "$installs/dummy/.2.0.0.mise-migrating"
assert_contains "mise installs migrate --dry-run 2>&1" "then migrate it"
assert_contains "mise installs migrate dummy@2.0.0 2>&1" "restored"
assert "test -L '$installs/dummy/2.0.0'"
assert_empty "find '$installs/dummy' -maxdepth 1 -name '.*mise-migrating'"
assert_empty "ls '$installs/.mise/migrations'"
# as if a run stopped after the reinstall wrote its receipt and version link,
# but before it finished: neither proves the postinstall ran
assert "mise uninstall dummy@2.0.0"
assert "MISE_INSTALL_LAYOUT=legacy MISE_EXPERIMENTAL=0 mise install dummy@2.0.0"
echo old >"$installs/dummy/2.0.0/marker"
mv "$installs/dummy/2.0.0" "$installs/dummy/.2.0.0.mise-migrating"
assert "mise install dummy@2.0.0"
assert "test -L '$installs/dummy/2.0.0'"
journal 2.0.0 false "$(mise where dummy@2.0.0)"
assert_contains "mise installs migrate --dry-run 2>&1" "installs/dummy/2.0.0 from an interrupted migration, then migrate it"
assert_contains "mise installs migrate dummy@2.0.0 2>&1" "restored"
assert "test -f '$(mise where dummy@2.0.0)/.mise-install.toml'"
assert "test ! -e '$installs/dummy/2.0.0/marker'"
assert_empty "find '$installs/dummy' -maxdepth 1 -name '.*mise-migrating'"
# as if a run stopped after recording the migration finished, before removing
# the old directory
made="$(basename "$(mise where dummy@2.0.0)")"
journal 2.0.0 true
mkdir "$installs/dummy/.2.0.0.mise-migrating"
assert_contains "mise installs migrate 2>&1" "removed"
assert "test -f '$installs/$made/.mise-install.toml'"
assert "test -L '$installs/dummy/2.0.0'"
assert_empty "find '$installs/dummy' -maxdepth 1 -name '.*mise-migrating'"
assert_empty "ls '$installs/.mise/migrations'"
# an installation the interrupted run did not write into is left alone
journal 2.0.0 false
mkdir "$installs/dummy/.2.0.0.mise-migrating"
assert_contains "mise installs migrate 2>&1" "restored"
assert "test -f '$installs/$made/.mise-install.toml'"
rm -rf "$installs/dummy/2.0.0"
assert "mise install dummy@2.0.0"
# a journal that cannot be read does not block the next run
echo "not a journal [" >"$installs/.mise/migrations/dummy@2.0.0.toml"
assert_contains "mise installs migrate 2>&1" "cannot be read"
assert_empty "ls '$installs/.mise/migrations'"
# ...and an old directory left beside the version link, which may be a finished
# migration's, is kept rather than put back over it
echo "not a journal [" >"$installs/.mise/migrations/dummy@2.0.0.toml"
mkdir "$installs/dummy/.2.0.0.mise-migrating"
assert_contains "mise installs migrate 2>&1" "is kept"
assert "test -L '$installs/dummy/2.0.0'"
assert "test -d '$installs/dummy/.2.0.0.mise-kept'"
assert_empty "find '$installs/dummy' -maxdepth 1 -name '.*mise-migrating'"
rm -rf "$installs/dummy/.2.0.0.mise-kept"

echo "=== a legacy version with an installation already in the new layout keeps its path ==="
assert "MISE_INSTALL_LAYOUT=legacy MISE_EXPERIMENTAL=0 mise install tiny@3.0.0"
assert "mise install vfox:tiny@3.0.0"
assert "test -d '$installs/tiny/3.0.0' && test ! -L '$installs/tiny/3.0.0'"
assert "mise installs migrate tiny@3.0.0"
assert "test -L '$installs/tiny/3.0.0'"
assert "realpath '$installs/tiny/3.0.0'" "$(realpath "$(mise where vfox:tiny@3.0.0)")"

echo "=== an interrupted relocation is put back on the next run ==="
mkdir -p "$installs/dummy/other-dummy/bin"
echo moved >"$installs/dummy/other-dummy/marker"
assert_contains "mise installs migrate dummy@other-dummy 2>&1" "relocated dummy@other-dummy"
moved="$installs/$(basename "$(readlink "$installs/dummy/other-dummy")")"
# as if the run stopped after moving the directory and linking it, before it
# recorded the migration as finished
cat >"$installs/.mise/migrations/dummy@other-dummy.toml" <<EOF2
tool = "dummy"
legacy = "$installs/dummy/other-dummy"
backend = "$backend"
version = "other-dummy"
made = ["$moved"]
relocating = true
finished = false
EOF2
assert_contains "mise installs migrate --dry-run 2>&1" "then migrate it"
assert_contains "mise installs migrate dummy@other-dummy 2>&1" "restored"
# put back, then moved again by the same run
assert_contains "cat '$installs/dummy/other-dummy/marker'" "moved"
assert "test -L '$installs/dummy/other-dummy'"
assert_empty "ls '$installs/.mise/migrations'"
assert_empty "find '$installs/dummy' -maxdepth 1 -name '.*mise-migrating'"
