#!/usr/bin/env bash

mkdir -p seed fakebin remotes
git -C seed init -b main
git -C seed config user.email test@example.invalid
git -C seed config user.name Test
echo '[tools]' >seed/config.toml
git -C seed add .
git -C seed -c core.hooksPath=/dev/null commit -m first
first_revision=$(git -C seed rev-parse HEAD)
source_repo="$PWD/seed"
binary=$(command -v mise)
export FAKE_REMOTE_BASE="$PWD/remotes"
export SSH_LOG="$PWD/ssh.log"
cat >fakebin/ssh <<'EOF'
#!/bin/sh
printf '%s\n' "$*" >>"$SSH_LOG"
while test "$#" -gt 0; do
  case "$1" in
    -o|-p|-i|-S) shift 2 ;;
    -tt) shift ;;
    -O)
      if test "$2" = forward; then exit 1; fi
      if test "${BLOCK_CLEANUP:-}" = 1; then exec sleep 60; fi
      exit 0
      ;;
    *)
      destination=$1
      shift
      case "$1" in
        'tar -xzf '*|*' version')
          if test "${BLOCK_PROVISION_STEP:-}" = 1; then
            echo $$ >"$FAKE_REMOTE_BASE/$destination.pid"
            exec sleep 60
          fi
          ;;
        'rm -rf -- /tmp/mise-bootstrap.'*)
          if test "${FAIL_CLEANUP_ONCE:-}" = 1; then
            echo attempt >>"$FAKE_REMOTE_BASE/cleanup-attempts"
            if test ! -f "$FAKE_REMOTE_BASE/cleanup-failed"; then
              touch "$FAKE_REMOTE_BASE/cleanup-failed"
              exit 1
            fi
          fi
          if test "${BLOCK_CLEANUP:-}" = 1; then
            echo $$ >>"$FAKE_REMOTE_BASE/cleanup-pids"
            exec sleep 60
          fi
          ;;
        *repository.bundle*)
          if test "${BLOCK_BUNDLE_UPLOAD:-}" = 1; then
            echo $$ >"$FAKE_REMOTE_BASE/upload.pid"
            exec sleep 60
          fi
          ;;
      esac
      export HOME="$FAKE_REMOTE_BASE/$destination"
      export MISE_CONFIG_DIR="$HOME/.config/mise"
      unset MISE_GLOBAL_CONFIG_FILE
      if test "$destination" = explicit-global; then
        export MISE_GLOBAL_CONFIG_FILE="$MISE_CONFIG_DIR/config.toml"
      fi
      export MISE_DATA_DIR="$HOME/.local/share/mise"
      export MISE_CACHE_DIR="$HOME/.cache/mise"
      export XDG_STATE_HOME="$HOME/.local/state"
      export MISE_STATE_DIR="$HOME/.local/state/mise"
      unset MISE_GITHUB_TOKEN MISE_GITHUB_API_TOKEN GITHUB_TOKEN GH_TOKEN
      export MISE_EXPERIMENTAL=0
      mkdir -p "$MISE_CONFIG_DIR"
      cd "$HOME" || exit 1
      exec sh -c "$1"
      ;;
  esac
done
EOF
chmod +x fakebin/ssh
export PATH="$PWD/fakebin:$PATH"

mkdir -p remotes/one/.config/mise
echo '# machine local' >remotes/one/.config/mise/config.local.toml
# A dry run connects, inspects, and says what it would do without writing the
# checkout (a clone on an empty host, an adoption on one with files already);
# the bootstrap that follows is previewed too.
assert_contains "mise bootstrap remote --host two --adopt '$source_repo' --remote-mise '$binary' --only dotfiles --dry-run 2>&1" 'Would clone'
assert_contains "cat '$SSH_LOG'" '--repository-dry-run'
assert_contains "cat '$SSH_LOG'" 'bootstrap --dry-run'
test ! -e remotes/two/.config/mise/.git
assert_contains "mise bootstrap remote --host one --adopt '$source_repo' --remote-mise '$binary' --only dotfiles --dry-run 2>&1" 'Would adopt'
assert_contains "mise bootstrap remote --host one --from-git='$source_repo' --remote-mise '$binary' --only dotfiles --dry-run 2>&1" 'This will be removed in mise 2026.10.0.'
test ! -e remotes/one/.config/mise/.git
assert "cat remotes/one/.config/mise/config.local.toml" '# machine local'
mise bootstrap remote --host one --host two --adopt "$source_repo" --remote-mise "$binary" --only dotfiles --yes
assert "git -C remotes/one/.config/mise rev-parse HEAD" "$first_revision"
assert "git -C remotes/two/.config/mise rev-parse HEAD" "$first_revision"
assert "git -C remotes/one/.config/mise config remote.origin.url" "$source_repo"
assert "cat remotes/one/.config/mise/config.local.toml" '# machine local'
assert_contains "cat '$SSH_LOG'" '/.config/mise bootstrap'
test ! -d "$HOME/.config/mise/.git"

echo '# second' >>seed/config.toml
git -C seed add .
git -C seed -c core.hooksPath=/dev/null commit -m second
second_revision=$(git -C seed rev-parse HEAD)
mise bootstrap remote --host one --adopt "$source_repo" --remote-mise "$binary" --only dotfiles --yes
assert "git -C remotes/one/.config/mise rev-parse HEAD" "$first_revision"
# an update is previewed against the existing checkout without moving it
assert_contains "mise bootstrap remote --host one --adopt '$source_repo' --remote-mise '$binary' --only dotfiles --yes --update --dry-run 2>&1" 'Would fast-forward'
assert "git -C remotes/one/.config/mise rev-parse HEAD" "$first_revision"
assert_fail "git -C remotes/one/.config/mise cat-file -e $second_revision^{commit}"
mise bootstrap remote --host one --adopt "$source_repo" --remote-mise "$binary" --only dotfiles --yes --update
assert "git -C remotes/one/.config/mise rev-parse HEAD" "$second_revision"
assert "git -C remotes/one/.config/mise rev-parse '@{upstream}'" "$second_revision"
assert "cat remotes/one/.config/mise/config.local.toml" '# machine local'
echo '# dirty' >>remotes/one/.config/mise/config.toml
assert_fail "mise bootstrap remote --host one --adopt '$source_repo' --remote-mise '$binary' --only dotfiles --yes --update" 'uncommitted changes'
assert_fail "mise bootstrap remote --host one --adopt '$source_repo' --remote-mise '$binary' --only dotfiles --yes --update --dry-run" 'uncommitted changes'

# A failed forwarding request must prevent bootstrap and close the owned master.
assert_fail "MISE_GITHUB_TOKEN=fake-local-relay-token mise bootstrap remote --host two --adopt '$source_repo' --remote-mise '$binary' --only dotfiles --yes --github-relay-read-only --github-relay-repo owner/repo" 'forwarding failed'
assert_contains "cat '$SSH_LOG'" '-O exit'
assert_not_contains "cat '$SSH_LOG'" 'fake-local-relay-token'

# Both archive upload and executable provisioning must yield to cancellation.
for step in archive provision; do
  if test "$step" = archive; then
    source_args=(--source "$source_repo")
  else
    source_args=(--adopt "$source_repo")
  fi
  BLOCK_PROVISION_STEP=1 mise bootstrap remote --host "$step" "${source_args[@]}" --remote-mise "$binary" --only dotfiles --yes &
  bootstrap_pid=$!
  wait_for_file "$FAKE_REMOTE_BASE/$step.pid" "blocked $step" 10 "$bootstrap_pid"
  step_pid=$(cat "$FAKE_REMOTE_BASE/$step.pid")
  kill -INT "$bootstrap_pid"
  step_status=0
  wait "$bootstrap_pid" || step_status=$?
  test "$step_status" -eq 130
  assert_fail "kill -0 '$step_pid' 2>/dev/null"
done

# A stalled bundle transfer must not block cancellation or leave its SSH child alive.
BLOCK_BUNDLE_UPLOAD=1 mise bootstrap remote --host stalled --adopt "$source_repo" --remote-mise "$binary" --only dotfiles --yes &
bootstrap_pid=$!
wait_for_file "$FAKE_REMOTE_BASE/upload.pid" 'blocked bundle upload' 10 "$bootstrap_pid"
upload_pid=$(cat "$FAKE_REMOTE_BASE/upload.pid")
kill -TERM "$bootstrap_pid"
wait "$bootstrap_pid" && exit 1
for _ in {1..50}; do
  kill -0 "$upload_pid" 2>/dev/null || break
  sleep 0.1
done
assert_fail "kill -0 '$upload_pid' 2>/dev/null"

# Failed cleanup is retried before closing the owned control session.
FAIL_CLEANUP_ONCE=1 mise bootstrap remote --host retry-cleanup --adopt "$source_repo" --remote-mise "$binary" --only dotfiles --yes
assert "cat '$FAKE_REMOTE_BASE/cleanup-attempts'" 'attempt
attempt'

# Cleanup and control-close commands themselves cannot hang cancellation.
BLOCK_BUNDLE_UPLOAD=1 BLOCK_CLEANUP=1 mise bootstrap remote --host blocked-cleanup --adopt "$source_repo" --remote-mise "$binary" --only dotfiles --yes &
bootstrap_pid=$!
for _ in {1..100}; do
  test -f "$FAKE_REMOTE_BASE/upload.pid" && test "$(cat "$FAKE_REMOTE_BASE/upload.pid")" != "$upload_pid" && break
  sleep 0.1
done
kill -TERM "$bootstrap_pid"
for _ in {1..300}; do
  kill -0 "$bootstrap_pid" 2>/dev/null || break
  sleep 0.1
done
assert_fail "kill -0 '$bootstrap_pid' 2>/dev/null"
wait "$bootstrap_pid" && exit 1
while read -r cleanup_pid; do
  assert_fail "kill -0 '$cleanup_pid' 2>/dev/null"
done <"$FAKE_REMOTE_BASE/cleanup-pids"

# Local Git fetching is cancellable too; credentials/helpers need not finish.
export REAL_GIT
REAL_GIT=$(command -v git)
cat >fakebin/git <<'EOF'
#!/bin/sh
is_clone() {
  while test "${1:-}" = -c; do
    shift 2
  done
  test "${1:-}" = clone
}
if is_clone "$@" && test "${BLOCK_FETCH:-}" = 1; then
  echo $$ >"$FAKE_REMOTE_BASE/fetch.pid"
  exec sleep 60
fi
exec "$REAL_GIT" "$@"
EOF
chmod +x fakebin/git
BLOCK_FETCH=1 mise bootstrap remote --host fetch-cancel --adopt "$source_repo" --remote-mise "$binary" --yes &
bootstrap_pid=$!
wait_for_file "$FAKE_REMOTE_BASE/fetch.pid" 'blocked repository fetch' 10 "$bootstrap_pid"
fetch_pid=$(cat "$FAKE_REMOTE_BASE/fetch.pid")
kill -INT "$bootstrap_pid"
wait "$bootstrap_pid" && exit 1
for _ in {1..50}; do
  kill -0 "$fetch_pid" 2>/dev/null || break
  sleep 0.1
done
assert_fail "kill -0 '$fetch_pid' 2>/dev/null"

# Even an authenticated HTTPS origin must not redirect Git onto plaintext HTTP.
openssl req -x509 -newkey rsa:2048 -nodes -keyout redirect.key -out redirect.crt -days 1 -subj /CN=localhost >/dev/null 2>&1
python3 "$ROOT/e2e/fixtures/git_redirect.py" redirect.crt redirect.key &
redirect_pid=$!
trap 'kill "$redirect_pid" 2>/dev/null || true' EXIT
wait_for_file redirect-port 'HTTPS redirect fixture' 10 "$redirect_pid"
# This test certificate is self-signed; disable verification only for this fixture.
assert_fail "GIT_SSL_NO_VERIFY=1 mise bootstrap remote --host redirect --adopt 'https://127.0.0.1:$(cat redirect-port)/repo' --remote-mise '$binary' --yes" 'could not fetch setup repository'

# Local and remote tracking work without experimental opt-in.
export MISE_EXPERIMENTAL=0
git init -q --bare -b main "$PWD/tracked-origin.git"
printf 'tracked contents\n' >~/.remote-tracked
assert_succeed 'mise bootstrap dotfiles track ~/.remote-tracked'
assert_succeed "mise bootstrap dotfiles origin set file://$PWD/tracked-origin.git --sync manual --yes"
assert_succeed 'mise bootstrap dotfiles sync'
# Local tracked files do not interfere with an unrelated source archive.
assert_succeed "mise bootstrap remote --host ordinary-source --source seed --remote-mise '$binary' --only dotfiles --yes"
assert_succeed "mise bootstrap remote --host tracked --adopt file://$PWD/tracked-origin.git --remote-mise '$binary' --only dotfiles --dry-run"
assert_fail 'test -f remotes/tracked/.config/mise/config.local.toml'
assert_succeed "mise bootstrap remote --host tracked --adopt file://$PWD/tracked-origin.git --remote-mise '$binary' --only dotfiles --yes"
assert_not_contains 'cat remotes/tracked/.config/mise/config.local.toml' 'experimental = true'
assert 'cat remotes/tracked/.remote-tracked' 'tracked contents'
assert_contains 'cat remotes/tracked/.config/mise/config.local.toml' '[history.origin]'
assert "ssh tracked 'unset MISE_EXPERIMENTAL; $binary settings get experimental'" 'false'
mkdir -p remotes/conflicted
printf 'keep my edit\n' >remotes/conflicted/.remote-tracked
assert_fail "mise bootstrap remote --host conflicted --adopt file://$PWD/tracked-origin.git --remote-mise '$binary' --only dotfiles --yes"
assert_not_contains 'cat remotes/conflicted/.config/mise/config.local.toml' 'experimental = true'
test -f redirect-requested
test ! -e redirect-followed
kill "$redirect_pid"
wait "$redirect_pid" || true
