#!/bin/sh
# Native Windows VM controller for the Helium VirtualBox smoke fixture.
#
# This intentionally runs on the Linux controller.  VBoxManage and the
# password file stay on the Linux VirtualBox host, reached only over SSH.  The
# VM's GUI-subsystem rvbox.exe is never started directly by Guest Control:
# Guest Control runs console-safe management programs, while SCM runs the real
# service process.
set -eu

repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)

usage() {
    cat <<'EOF'
usage: scripts/windows/test-host ACTION [--run-id ID] [--bundle DIRECTORY] [--endpoint HOST:PORT] [--fail-contexts LIST]

Actions:
  status   read-only VM/snapshot identity and state check
  prepare  restore the declared baseline, boot headless, and verify Guest Additions
  stage    copy a bundle containing rvbox.exe and client.toml into the guest test root
  install  install and start RVBox from the staged bundle through a fixture-only full-admin principal
  run      start the already-installed RVBox SCM service from the staged bundle
  logoff   log off the sole active fixture user; use only after service installation
  collect  copy bounded guest artifacts to the local test-run directory
  inspect  read-only RVBox SCM state and bounded client log from a prepared run
  logs     read-only service-startup and client logs from a prepared run
  stop     stop RVBox through SCM and request a graceful guest shutdown
  reset    stop the guest if necessary, restore the declared baseline, and leave it off
  recover  read-only fixture/run-state check for a stopped-resumable run

Optional environment:
  The documented Helium fixture identity and password-file path are defaults.
  RVBOX_TEST_VBOX_HOST, RVBOX_TEST_VBOX_VM, RVBOX_TEST_VBOX_VM_UUID,
  RVBOX_TEST_VBOX_SNAPSHOT, RVBOX_TEST_VBOX_SNAPSHOT_UUID,
  RVBOX_TEST_GUEST_USER, RVBOX_TEST_GUEST_PASSWORD_FILE (overrides)
  RVBOX_TEST_PROVISIONER_USER, RVBOX_TEST_PROVISIONER_PASSWORD_FILE
    (default Administrator and the documented fixture password file)
  RVBOX_TEST_HOST_STAGE_ROOT (default /home/cabbage/.local/state/rvbox-test-runs)
  RVBOX_TEST_RUN_ROOT        (default .test-runs/windows-vm)
  RVBOX_TEST_ACCEL_HTTP_URL, RVBOX_TEST_ACCEL_HTTP_AUTH,
  RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR, RVBOX_TEST_ACCEL_SOCKS5
    (documented accelerated HTTP stage route; empty URL disables it)
EOF
}

fail() { printf '%s\n' "test-host: $*" >&2; exit 2; }

require_env() {
    eval "value=\${$1-}"
    [ -n "$value" ] || fail "$1 is required"
}

safe_word() {
    case $2 in
        ''|*[!A-Za-z0-9._:/@+=,-]*) fail "$1 contains unsupported characters" ;;
    esac
}

safe_id() {
    case $1 in
        [a-z0-9]* ) ;;
        * ) fail "run ID must start with lowercase alphanumeric" ;;
    esac
    case $1 in
        *[!a-z0-9-]*|????????????????????????????????????????????????????????????????*)
            fail "run ID must match [a-z0-9][a-z0-9-]{0,63}"
            ;;
    esac
}

action=${1-}
[ -n "$action" ] || { usage >&2; exit 2; }
case $action in --help|-h) usage; exit 0 ;; esac
shift

run_id=
bundle=
endpoint=
fail_contexts=
while [ "$#" -gt 0 ]; do
    case $1 in
        --run-id) [ "$#" -ge 2 ] || fail "--run-id needs a value"; run_id=$2; shift 2 ;;
        --bundle) [ "$#" -ge 2 ] || fail "--bundle needs a value"; bundle=$2; shift 2 ;;
        --endpoint) [ "$#" -ge 2 ] || fail "--endpoint needs a value"; endpoint=$2; shift 2 ;;
        --fail-contexts) [ "$#" -ge 2 ] || fail "--fail-contexts needs a value"; fail_contexts=$2; shift 2 ;;
        --help|-h) usage; exit 0 ;;
        *) fail "unknown argument $1" ;;
    esac
done

case $action in status|prepare|stage|install|run|logoff|collect|inspect|logs|stop|reset|recover) ;; *) usage >&2; fail "unknown action $action" ;; esac
if [ "$action" != status ]; then
    [ -n "$run_id" ] || fail "$action requires --run-id"
    safe_id "$run_id"
fi
if [ "$action" = stage ]; then
    [ -d "$bundle" ] || fail "stage requires an existing --bundle directory"
    [ -f "$bundle/rvbox.exe" ] || fail "bundle must contain rvbox.exe"
    [ -f "$bundle/client.toml" ] || fail "bundle must contain client.toml"
fi
if [ -n "$endpoint" ]; then safe_word endpoint "$endpoint"; fi
if [ -n "$fail_contexts" ]; then safe_word fail_contexts "$fail_contexts"; fi

# The Helium smoke fixture is the only supported native lane today.  Keep its
# non-secret identity and host-local password-file *path* here so a developer
# can run the controller without retyping fixture metadata.  Operators may
# override any value for another recorded fixture.  The password itself is
# never read by this script and is never stored in the repository.
: "${RVBOX_TEST_VBOX_HOST:=helium-remote}"
: "${RVBOX_TEST_VBOX_VM:=rvbox-win10-test}"
: "${RVBOX_TEST_VBOX_VM_UUID:=6cdc114f-71e5-4167-a394-e922e14e6f5c}"
: "${RVBOX_TEST_VBOX_SNAPSHOT:=baseline-clean-administrator}"
: "${RVBOX_TEST_VBOX_SNAPSHOT_UUID:=ba5ce5f1-77e3-44b0-8d91-534becce27ff}"
: "${RVBOX_TEST_GUEST_USER:=rvboxtest}"
: "${RVBOX_TEST_GUEST_PASSWORD_FILE:=/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test.password}"
: "${RVBOX_TEST_PROVISIONER_USER:=Administrator}"
: "${RVBOX_TEST_PROVISIONER_PASSWORD_FILE:=$RVBOX_TEST_GUEST_PASSWORD_FILE}"
: "${RVBOX_TEST_ACCEL_HTTP_URL:=http://x1.xcel.me:9124}"
: "${RVBOX_TEST_ACCEL_HTTP_AUTH:=x1:x1}"
: "${RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR:=/home/ubuntu/Downloads}"
: "${RVBOX_TEST_ACCEL_SOCKS5:=socks5h://127.0.0.1:1085}"
provisioner_user=$RVBOX_TEST_PROVISIONER_USER
provisioner_password_file=$RVBOX_TEST_PROVISIONER_PASSWORD_FILE
accelerated_artifact=

for name in RVBOX_TEST_VBOX_HOST RVBOX_TEST_VBOX_VM RVBOX_TEST_VBOX_VM_UUID \
    RVBOX_TEST_VBOX_SNAPSHOT RVBOX_TEST_VBOX_SNAPSHOT_UUID \
    RVBOX_TEST_GUEST_USER RVBOX_TEST_GUEST_PASSWORD_FILE; do
    require_env "$name"
done

safe_word RVBOX_TEST_VBOX_HOST "$RVBOX_TEST_VBOX_HOST"
safe_word RVBOX_TEST_VBOX_VM "$RVBOX_TEST_VBOX_VM"
safe_word RVBOX_TEST_VBOX_VM_UUID "$RVBOX_TEST_VBOX_VM_UUID"
safe_word RVBOX_TEST_VBOX_SNAPSHOT "$RVBOX_TEST_VBOX_SNAPSHOT"
safe_word RVBOX_TEST_VBOX_SNAPSHOT_UUID "$RVBOX_TEST_VBOX_SNAPSHOT_UUID"
safe_word RVBOX_TEST_GUEST_USER "$RVBOX_TEST_GUEST_USER"
safe_word RVBOX_TEST_GUEST_PASSWORD_FILE "$RVBOX_TEST_GUEST_PASSWORD_FILE"
if [ -n "$provisioner_user" ]; then safe_word RVBOX_TEST_PROVISIONER_USER "$provisioner_user"; fi
if [ -n "$provisioner_password_file" ]; then safe_word RVBOX_TEST_PROVISIONER_PASSWORD_FILE "$provisioner_password_file"; fi
if [ -n "$RVBOX_TEST_ACCEL_HTTP_URL" ]; then
    safe_word RVBOX_TEST_ACCEL_HTTP_URL "$RVBOX_TEST_ACCEL_HTTP_URL"
    safe_word RVBOX_TEST_ACCEL_HTTP_AUTH "$RVBOX_TEST_ACCEL_HTTP_AUTH"
    safe_word RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR "$RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR"
    safe_word RVBOX_TEST_ACCEL_SOCKS5 "$RVBOX_TEST_ACCEL_SOCKS5"
    case $RVBOX_TEST_ACCEL_HTTP_URL in http://*|https://*) ;; *) fail "RVBOX_TEST_ACCEL_HTTP_URL must use http(s)" ;; esac
    case $RVBOX_TEST_ACCEL_SOCKS5 in socks5://*|socks5h://*) ;; *) fail "RVBOX_TEST_ACCEL_SOCKS5 must use socks5" ;; esac
fi

host_stage_root=${RVBOX_TEST_HOST_STAGE_ROOT:-/home/cabbage/.local/state/rvbox-test-runs}
run_root=${RVBOX_TEST_RUN_ROOT:-$repo_root/.test-runs/windows-vm}
safe_word RVBOX_TEST_HOST_STAGE_ROOT "$host_stage_root"
remote_run_id=${run_id:-fixture-status}
host_stage=$host_stage_root/$remote_run_id
# This value crosses a remote POSIX shell before Guest Control. Windows accepts
# forward slashes, which avoids backslash loss while SSH reconstructs argv.
guest_root="C:/ProgramData/RVBox/test-runs/$remote_run_id"

remote() {
    # All values below are constrained words before becoming remote shell
    # arguments. Password contents are never transmitted or printed; only the
    # approved host-local password-file path is passed to VBoxManage. Execute
    # the helper through this one SSH connection: the former upload-then-run
    # scheme could race with a stale controller that removed the shared helper
    # filename between those two connections.
    remote_action=$1
    retry_limit=1
    # These operations are either read-only or converge on the same staged
    # artifact/service configuration. A lost SSH response is therefore safe to
    # retry. Lifecycle transitions remain single-attempt: their caller must
    # inspect/recover rather than risk a duplicate reset, shutdown, or logoff.
    case $remote_action in
        status|recover|prepare-stage|stage|stage-create-root|stage-copy-exe|stage-copy-config|stage-copy-ca|collect|inspect|logs|install|run)
            retry_limit=4
            ;;
    esac
    remote_endpoint=${endpoint:--}
    remote_fail_contexts=${fail_contexts:--}
    retry_attempt=1
    while [ "$retry_attempt" -le "$retry_limit" ]; do
        if ssh -o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2 "$RVBOX_TEST_VBOX_HOST" \
        "sh -s -- '$1' '$RVBOX_TEST_VBOX_VM' '$RVBOX_TEST_VBOX_VM_UUID' '$RVBOX_TEST_VBOX_SNAPSHOT' '$RVBOX_TEST_VBOX_SNAPSHOT_UUID' '$RVBOX_TEST_GUEST_USER' '$RVBOX_TEST_GUEST_PASSWORD_FILE' '$host_stage' '$guest_root' '$remote_endpoint' '$provisioner_user' '$provisioner_password_file' '$remote_fail_contexts'" <<'REMOTE'
set -eu

action=$1
vm=$2
expected_vm_uuid=$3
snapshot=$4
expected_snapshot_uuid=$5
guest_user=$6
password_file=$7
host_stage=$8
guest_root=$9
shift 9
endpoint=$1
provisioner_user=$2
provisioner_password_file=$3
fail_contexts=$4
[ "$endpoint" = - ] && endpoint=
[ "$fail_contexts" = - ] && fail_contexts=

fail() { printf '%s\n' "remote test-host: $*" >&2; exit 2; }

lease_root=$(dirname "$host_stage")/.rvbox-windows-vm-lease
lease_owner=$lease_root/run-id
run_id=$(basename "$host_stage")

acquire_lease() {
    install -d -m 700 "$(dirname "$lease_root")"
    if mkdir "$lease_root" 2>/dev/null; then
        umask 077
        printf '%s\n' "$run_id" >"$lease_owner"
        return 0
    fi
    [ -f "$lease_owner" ] || fail "Windows VM lease is malformed: $lease_root"
    owner=$(cat "$lease_owner")
    [ "$owner" = "$run_id" ] || fail "Windows VM is leased by run $owner"
}

require_lease() {
    [ -f "$lease_owner" ] || fail "Windows VM lease is missing"
    owner=$(cat "$lease_owner")
    [ "$owner" = "$run_id" ] || fail "Windows VM is leased by run $owner"
}

release_lease() {
    require_lease
    rm "$lease_owner"
    rmdir "$lease_root"
}

step() {
    install -d -m 700 "$host_stage"
    printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >>"$host_stage/controller.steps"
}

vm_field() {
    VBoxManage showvminfo "$vm" --machinereadable | sed -n "s/^$1=\"\([^\"]*\)\"/\1/p" | head -n 1
}

assert_identity() {
    actual_vm_uuid=$(vm_field UUID)
    [ "$actual_vm_uuid" = "$expected_vm_uuid" ] || fail "VM UUID mismatch"
    actual_snapshot_uuid=$(VBoxManage snapshot "$vm" list --machinereadable | sed -n 's/^CurrentSnapshotUUID="\([^"]*\)"/\1/p')
    [ "$actual_snapshot_uuid" = "$expected_snapshot_uuid" ] || fail "current snapshot UUID mismatch"
}

state() { vm_field VMState; }

guest_run() {
    # This VirtualBox build has no --wait-exit.  It can return 33 after a
    # successful guest process, so each caller must emit RVBOX_GUEST_OK only
    # after its own assertion succeeds.  Never treat the VBoxManage exit code
    # alone as a guest-command result.
    output=$(VBoxManage guestcontrol "$vm" --username "$guest_user" --passwordfile "$password_file" \
        run "$@" </dev/null 2>&1) || true
    printf '%s\n' "$output"
    printf '%s\n' "$output" | tr -d '\r' | grep -qx 'RVBOX_GUEST_OK'
}

provisioner_run() {
    # The clean baseline deliberately has no RVBox service.  Guest Control's
    # normal test account has a filtered UAC token, so only the fixture-only
    # full-token administrator may perform the first machine-wide install.
    [ -n "$provisioner_user" ] || fail "install requires RVBOX_TEST_PROVISIONER_USER"
    [ -n "$provisioner_password_file" ] || fail "install requires RVBOX_TEST_PROVISIONER_PASSWORD_FILE"
    output=$(VBoxManage guestcontrol "$vm" --username "$provisioner_user" --passwordfile "$provisioner_password_file" \
        run "$@" </dev/null 2>&1) || true
    printf '%s\n' "$output"
    printf '%s\n' "$output" | tr -d '\r' | grep -qx 'RVBOX_GUEST_OK'
}

assert_provisioner_elevated() {
    # This fixture is en-US.  Check the mandatory label before allowing any
    # machine-wide mutation, so an accidentally filtered automation account
    # fails closed instead of silently weakening the test contract.
    provisioner_run --exe 'C:\\Windows\\System32\\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
        /d /s /c 'whoami /groups | findstr /c:"High Mandatory Level" >NUL && echo RVBOX_GUEST_OK' >/dev/null || \
        fail "fixture provisioner is not a full high-integrity administrator"
}

assert_clean_guest() {
    # A missing service is the authoritative clean-baseline condition.  The
    # test service name is unique, so do not delete or alter any other service.
    guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
        /d /s /c 'sc.exe query RVBoxClient >NUL 2>&1 & if errorlevel 1060 (echo RVBOX_GUEST_OK) else exit /b 1' >/dev/null || \
        fail "reset baseline is not clean: RVBoxClient is already installed"
}

assert_staged_guest() {
    guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
        /d /s /c "if exist \"$guest_root\\rvbox.exe\" if exist \"$guest_root\\client.toml\" echo RVBOX_GUEST_OK" >/dev/null || \
        fail "staged guest bundle is missing rvbox.exe or client.toml"
}

assert_provisioner_absent() {
    # A second logged-on Administrator could become an additional WTS active
    # candidate and invalidate ACTIVE_* selection tests.  Do not guess which
    # account the supervisor would choose: fail before dispatch and reset.
    guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
        /d /s /c "query user | findstr /i /c:\"$provisioner_user\" >NUL & if errorlevel 1 echo RVBOX_GUEST_OK" >/dev/null || \
        fail "fixture provisioner remains logged on; reset before active-session tests"
}

wait_guest_additions() {
    attempt=0
    while [ "$attempt" -lt 60 ]; do
        properties=$(VBoxManage guestproperty enumerate "$vm" 2>/dev/null || true)
        if printf '%s\n' "$properties" | grep -q '/VirtualBox/GuestAdd/Version' && \
            printf '%s\n' "$properties" | grep -q '/VirtualBox/GuestInfo/OS/Release'; then
            return 0
        fi
        attempt=$((attempt + 1))
        sleep 1
    done
    fail "Guest Additions did not publish version and Windows OS-release properties"
}

configure_display_keepalive() {
    # VirtualBox VRDE reports a zero-bpp framebuffer when Windows powers off
    # the virtual monitor.  That leaves an authenticated Guacamole tunnel in
    # its "Waiting for response" state even though RDP negotiation succeeded.
    # Keep the disposable diagnostic fixture awake; this is deliberately
    # applied after every snapshot restore because power-plan state belongs to
    # the guest snapshot, not to the controller.
    provisioner_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
        /d /s /c '(powercfg /change monitor-timeout-ac 0 && powercfg /change monitor-timeout-dc 0 && powercfg /change standby-timeout-ac 0 && powercfg /change standby-timeout-dc 0 && powercfg /change hibernate-timeout-ac 0 && powercfg /change hibernate-timeout-dc 0 && echo RVBOX_GUEST_OK)' >/dev/null || \
        fail "fixture provisioner could not disable disposable display/sleep timers"
}

wait_service() {
    wanted=$1
    attempt=0
    while [ "$attempt" -lt 30 ]; do
        if guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
            /d /s /c "sc.exe query RVBoxClient | findstr /c:\"$wanted\" >NUL && echo RVBOX_GUEST_OK" >/dev/null 2>&1; then
            return 0
        fi
        attempt=$((attempt + 1))
        sleep 1
    done
    fail "RVBoxClient did not reach $wanted"
}

wait_service_stopped() {
	attempt=0
	while [ "$attempt" -lt 30 ]; do
		if guest_run --exe 'C:\\Windows\\System32\\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
			/d /s /c 'sc.exe query RVBoxClient | findstr /c:"STOPPED" >NUL && echo RVBOX_GUEST_OK' >/dev/null 2>&1; then
			return 0
		fi
		attempt=$((attempt + 1))
		sleep 1
	done
	fail "RVBoxClient did not reach STOPPED"
}

case "$action" in
    prepare-stage)
        assert_identity
        require_lease
        [ "$(state)" = running ] || fail "stage requires a running prepared VM"
        install -d -m 700 "$host_stage"
        ;;
    status)
        assert_identity
        printf 'vm=%s uuid=%s snapshot=%s state=%s\n' "$vm" "$expected_vm_uuid" "$snapshot" "$(state)"
        ;;
    prepare)
        assert_identity
        acquire_lease
        step prepare-lease-acquired
        [ "$(state)" = poweroff ] || fail "prepare requires a powered-off VM; use stop or reset first"
        VBoxManage snapshot "$vm" restore "$snapshot" >/dev/null
        step prepare-snapshot-restored
        assert_identity
        VBoxManage startvm "$vm" --type headless >/dev/null
        step prepare-vm-started
        wait_guest_additions
        step prepare-guest-additions-ready
        configure_display_keepalive
        step prepare-display-keepalive
        assert_clean_guest
        step prepare-clean-baseline-verified
        ;;
    probe-identity)
        assert_identity
        require_lease
        [ "$(state)" = running ] || fail "identity probe requires a running prepared VM"
        guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
            /d /s /c 'whoami /groups & query user & echo RVBOX_GUEST_OK' >/dev/null
        ;;
    stage)
        assert_identity
        require_lease
        [ "$(state)" = running ] || fail "stage requires a running prepared VM"
        [ -f "$host_stage/rvbox.exe" ] && [ -f "$host_stage/client.toml" ] || fail "host bundle is incomplete"
        ;;
    stage-create-root)
        assert_identity
        require_lease
        [ "$(state)" = running ] || fail "stage requires a running prepared VM"
        guest_run --exe 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' --wait-stdout --wait-stderr --unquoted-args -- \
            -NoProfile -NonInteractive -Command "New-Item -ItemType Directory -Force -Path '$guest_root','C:/ProgramData/RVBox/test-work','C:/ProgramData/RVBox/test-logs' | Out-Null; Write-Output RVBOX_GUEST_OK" >/dev/null
        ;;
    stage-copy-exe)
        assert_identity
        require_lease
        VBoxManage guestcontrol "$vm" --username "$guest_user" --passwordfile "$password_file" \
            copyto "$host_stage/rvbox.exe" "$guest_root\\rvbox.exe" </dev/null
        ;;
    stage-copy-config)
        assert_identity
        require_lease
        VBoxManage guestcontrol "$vm" --username "$guest_user" --passwordfile "$password_file" \
            copyto "$host_stage/client.toml" "$guest_root\\client.toml" </dev/null
        ;;
    stage-copy-ca)
        assert_identity
        require_lease
        [ -f "$host_stage/ca.pem" ] || fail "host bundle has no ca.pem"
        VBoxManage guestcontrol "$vm" --username "$guest_user" --passwordfile "$password_file" \
            copyto "$host_stage/ca.pem" "$guest_root\\ca.pem" </dev/null
        ;;
    install)
        assert_identity
        require_lease
        [ "$(state)" = running ] || fail "install requires a running prepared VM"
        assert_clean_guest
        assert_staged_guest
        assert_provisioner_elevated
        # Guest Control cannot reliably wait for GUI-subsystem rvbox.exe.  It
        # may report a non-zero wrapper result after the process has started,
        # therefore SCM state is the completion proof for this exact install.
        VBoxManage guestcontrol "$vm" --username "$provisioner_user" --passwordfile "$provisioner_password_file" \
            run --exe "$guest_root\\rvbox.exe" --unquoted-args -- \
            --install-service --config "$guest_root\\client.toml" </dev/null >/dev/null 2>&1 || true
        wait_service RUNNING
        assert_provisioner_absent
        step install-scm-service-running
        printf 'service=RVBoxClient state=RUNNING install=clean-baseline\n'
        ;;
    run)
        assert_identity
        require_lease
        [ "$(state)" = running ] || fail "run requires a running prepared VM"
        assert_staged_guest
        assert_provisioner_elevated
        if [ -n "$endpoint" ]; then
            endpoint_host=${endpoint%:*}
            endpoint_port=${endpoint##*:}
            guest_run --exe 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' --wait-stdout --wait-stderr --unquoted-args -- \
                -NoProfile -NonInteractive -Command "if (-not (Test-NetConnection -ComputerName '$endpoint_host' -Port $endpoint_port -InformationLevel Quiet)) { exit 1 }; Write-Output RVBOX_GUEST_OK" >/dev/null
        fi
		# Reconfigure from a stopped service so a controlled fixture fault cannot
		# leak across hierarchy rows. The release build rejects this argument;
		# only the separately tagged disposable test binary accepts it.
		provisioner_run --exe 'C:\\Windows\\System32\\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
			/d /s /c '(sc.exe stop RVBoxClient >NUL 2>&1 || sc.exe query RVBoxClient | findstr /c:"STOPPED" >NUL) && echo RVBOX_GUEST_OK' >/dev/null || true
		wait_service_stopped
		image="\\\"$guest_root\\rvbox.exe\\\" --service --config \\\"$guest_root\\client.toml\\\""
		if [ -n "$fail_contexts" ]; then image="$image --test-fail-contexts $fail_contexts"; fi
        provisioner_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
            /d /s /c "sc.exe query RVBoxClient >NUL 2>&1 && echo RVBOX_GUEST_OK" >/dev/null || \
            fail "RVBoxClient is not installed; run install from the clean baseline first"
        provisioner_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
            /d /s /c "sc.exe config RVBoxClient binPath= \"$image\" start= demand >NUL 2>&1 && echo RVBOX_GUEST_OK" >/dev/null || \
            fail "fixture provisioner could not change RVBoxClient configuration"
        provisioner_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
            /d /s /c '(sc.exe start RVBoxClient >NUL 2>&1 || sc.exe query RVBoxClient | findstr /c:"RUNNING" >NUL) && echo RVBOX_GUEST_OK' >/dev/null || \
            fail "fixture provisioner could not start RVBoxClient"
        wait_service RUNNING
        printf 'service=RVBoxClient state=RUNNING\n'
        ;;
	logoff)
		assert_identity
		require_lease
		[ "$(state)" = running ] || fail "logoff requires a running prepared VM"
		# The clean fixture has exactly one active console account. Logging it off
		# leaves the LocalSystem service alive and makes the no-user hierarchy
		# rows observable without introducing a second session candidate.  Do not
		# run `logoff` through that account's Guest Control channel: Windows ends
		# the channel before VBoxManage can return its completion sentinel.  The
		# fixture-only full-token provisioner is non-interactive (and separately
		# asserted absent from WTS candidates), so it can prove the transition.
		provisioner_run --exe 'C:\\Windows\\System32\\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
			/d /s /c "logoff 1 & echo RVBOX_GUEST_OK" >/dev/null
		attempt=0
		while [ "$attempt" -lt 30 ]; do
			if provisioner_run --exe 'C:\\Windows\\System32\\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
				/d /s /c "query user | findstr /i /c:\"$guest_user\" >NUL & if errorlevel 1 echo RVBOX_GUEST_OK" >/dev/null 2>&1; then
				step logoff-no-active-user
				printf 'session=none\n'
				break
			fi
			attempt=$((attempt + 1))
			sleep 1
		done
		[ "$attempt" -lt 30 ] || fail "fixture user did not log off"
		;;
    collect)
        assert_identity
        require_lease
        install -d -m 700 "$host_stage/artifacts"
        if [ "$(state)" = running ]; then
            provisioner_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
                /d /s /c "sc.exe queryex RVBoxClient > \"$guest_root\\service-status.txt\" 2>&1 & if exist \"C:/ProgramData/RVBox/service-startup.log\" copy /y \"C:/ProgramData/RVBox/service-startup.log\" \"$guest_root\\service-startup.log\" >NUL & if exist \"C:/ProgramData/RVBox/test-logs/rvbox.log\" copy /y \"C:/ProgramData/RVBox/test-logs/rvbox.log\" \"$guest_root\\rvbox.log\" >NUL & echo RVBOX_GUEST_OK" >/dev/null || true
            VBoxManage guestcontrol "$vm" --username "$provisioner_user" --passwordfile "$provisioner_password_file" \
                copyfrom "$guest_root" "$host_stage/artifacts" --recursive </dev/null >/dev/null 2>&1 || true
        fi
        printf 'collected host_stage=%s/artifacts\n' "$host_stage"
        ;;
    inspect)
        assert_identity
        require_lease
        [ "$(state)" = running ] || fail "inspect requires a running prepared VM"
        guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
            /d /s /c "sc.exe queryex RVBoxClient & sc.exe qc RVBoxClient & reg.exe query \"HKLM\\SYSTEM\\CurrentControlSet\\Services\\RVBoxClient\" /v ImagePath & reg.exe query \"HKLM\\SYSTEM\\CurrentControlSet\\Services\\RVBoxClient\" /v ObjectName & dir \"C:/ProgramData/RVBox\" & icacls \"C:/ProgramData/RVBox\" & certutil -hashfile \"$guest_root\\rvbox.exe\" SHA256 & \"$guest_root\\rvbox.exe\" --check-config --config \"$guest_root\\client.toml\" > \"$guest_root\\check-config.txt\" 2>&1 & type \"$guest_root\\check-config.txt\" & if exist \"C:/ProgramData/RVBox/service-startup.log\" type \"C:/ProgramData/RVBox/service-startup.log\" & wevtutil qe System /q:\"*[System[(EventID=7000 or EventID=7009 or EventID=7031 or EventID=7034)]]\" /c:3 /rd:true /f:text & if exist \"C:/ProgramData/RVBox/test-logs/rvbox.log\" type \"C:/ProgramData/RVBox/test-logs/rvbox.log\" & echo RVBOX_GUEST_OK"
        ;;
    logs)
        assert_identity
        require_lease
        [ "$(state)" = running ] || fail "logs requires a running prepared VM"
        provisioner_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
            /d /s /c "dir \"C:\\ProgramData\\RVBox\" & dir \"C:\\ProgramData\\RVBox\\test-logs\" & type \"C:\\ProgramData\\RVBox\\service-startup.log\" & type \"C:\\ProgramData\\RVBox\\test-logs\\rvbox.log\" & echo RVBOX_GUEST_OK"
        ;;
    stop)
        assert_identity
        require_lease
        if [ "$(state)" = running ]; then
            guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
                /d /s /c 'sc.exe stop RVBoxClient >NUL 2>&1 || exit /b 0 & echo RVBOX_GUEST_OK' >/dev/null || true
            VBoxManage controlvm "$vm" acpipowerbutton >/dev/null
            attempt=0
            while [ "$attempt" -lt 60 ]; do
                [ "$(state)" = poweroff ] && break
                attempt=$((attempt + 1))
                sleep 1
            done
            [ "$(state)" = poweroff ] || fail "guest did not power off after ACPI request"
        fi
        printf 'stopped vm=%s\n' "$vm"
        ;;
    reset)
        assert_identity
        # A controller may be interrupted after it has brought down its
        # Compose stack but before it removes local run files.  When the VM is
        # already powered off at the declared baseline and no lease exists,
        # reset is therefore a safe no-op.  It lets `native-test clean` repair
        # that abandoned local run without pretending it owns a live VM.
        if [ ! -f "$lease_owner" ]; then
            [ "$(state)" = poweroff ] || fail "reset requires the run lease while the VM is not powered off"
            printf 'reset vm=%s snapshot=%s already-clean\n' "$vm" "$snapshot"
            exit 0
        fi
        require_lease
        if [ "$(state)" = running ]; then
            VBoxManage controlvm "$vm" acpipowerbutton >/dev/null
            attempt=0
            while [ "$attempt" -lt 60 ]; do
                [ "$(state)" = poweroff ] && break
                attempt=$((attempt + 1))
                sleep 1
            done
            # This is the exact named disposable fixture, with the matching
            # run lease. Reset is its isolation boundary, not a graceful-stop
            # diagnostic command: after a bounded ACPI attempt, discard only
            # this guest's state so snapshot restore cannot strand the lane.
            if [ "$(state)" != poweroff ]; then
                printf 'reset: ACPI shutdown timed out; forcing disposable VM poweroff\n' >&2
                VBoxManage controlvm "$vm" poweroff >/dev/null
                attempt=0
                while [ "$attempt" -lt 30 ]; do
                    [ "$(state)" = poweroff ] && break
                    attempt=$((attempt + 1))
                    sleep 1
                done
                [ "$(state)" = poweroff ] || fail "guest did not power off after forced reset"
                printf 'reset-force-poweroff vm=%s\n' "$vm"
            fi
        fi
        VBoxManage snapshot "$vm" restore "$snapshot" >/dev/null
        assert_identity
        release_lease
        printf 'reset vm=%s snapshot=%s\n' "$vm" "$snapshot"
        ;;
    recover)
        assert_identity
        if [ -f "$lease_owner" ]; then
            printf 'recoverable vm=%s state=%s stage=%s lease_owner=%s\n' "$vm" "$(state)" "$host_stage" "$(cat "$lease_owner")"
        else
            printf 'recoverable vm=%s state=%s stage=%s lease_owner=none\n' "$vm" "$(state)" "$host_stage"
        fi
        ;;
esac
REMOTE
        then
            return 0
        fi
        retry_attempt=$((retry_attempt + 1))
        if [ "$retry_attempt" -le "$retry_limit" ]; then
            printf 'remote action=%s interrupted; retry %s/%s\n' "$remote_action" "$retry_attempt" "$retry_limit" >&2
            sleep 2
        fi
    done
    fail "remote action $remote_action exhausted $retry_limit SSH attempts"
}

# accelerated_stage uses the documented controller HTTP endpoint only for a
# compressed disposable test executable. The endpoint remains authenticated;
# the artifact name contains the run ID and payload digest and is deleted after
# successful guest staging. A failed HTTP attempt leaves no host executable
# change and fails the stage; the executable is never sent over the fragile
# SSH route.
accelerated_stage() {
    [ -d "$RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR" ] || {
        printf 'stage: accelerated publish directory unavailable\n' >&2
        return 1
    }
    # Keep all disposable test bytes inside the owning run directory; never
    # consume global /tmp, which may belong to a different test or user.
    stage_http_dir=$(mktemp -d "$run_root/.accelerated-stage.XXXXXX")
    stage_http_xz=$stage_http_dir/rvbox.exe.xz
    xz -T0 -3 -c "$bundle/rvbox.exe" >"$stage_http_xz"
    stage_http_hash=$(sha256sum "$stage_http_xz" | awk '{print $1}')
    stage_http_name="rvbox-$run_id-$stage_http_hash.xz"
    stage_http_published=$RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR/$stage_http_name
    if [ -e "$stage_http_published" ]; then
        [ ! -L "$stage_http_published" ] || fail "refusing symlink accelerated artifact $stage_http_published"
        existing_hash=$(sha256sum "$stage_http_published" | awk '{print $1}')
        [ "$existing_hash" = "$stage_http_hash" ] || fail "accelerated artifact name collision: $stage_http_published"
    else
        # The payload contains no configuration or credential. It is readable
        # only to the authenticated HTTP service so nginx can serve it.
        install -m 644 "$stage_http_xz" "$stage_http_published"
    fi
    rm -rf "$stage_http_dir"
    stage_http_url=$RVBOX_TEST_ACCEL_HTTP_URL/$stage_http_name
    printf 'stage: accelerated HTTP transfer\n'
    if ! ssh -o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2 "$RVBOX_TEST_VBOX_HOST" \
        "set -eu; stage='$host_stage'; target=\$stage/rvbox.exe.xz.http; curl --proxy '$RVBOX_TEST_ACCEL_SOCKS5' --anyauth -u '$RVBOX_TEST_ACCEL_HTTP_AUTH' --continue-at - --retry 4 --retry-all-errors --retry-delay 2 --connect-timeout 15 --max-time 120 --fail --silent --show-error --output \$target '$stage_http_url'; expected='$stage_http_hash'; actual=\$(sha256sum \$target | awk '{print \$1}'); test \"\$actual\" = \"\$expected\"; xz -dc \$target >\$stage/rvbox.exe.new; chmod 700 \$stage/rvbox.exe.new; mv \$stage/rvbox.exe.new \$stage/rvbox.exe; rm -f \$target"; then
        printf 'stage: accelerated HTTP transfer failed\n' >&2
        rm -f "$stage_http_published"
        return 1
    fi
    accelerated_artifact=$stage_http_published
    return 0
}

# Only small non-secret configuration files use the SSH control route. The
# executable itself always uses accelerated_stage above.
copy_stage_file() {
    source_file=$1
    target_name=$2
    copy_attempt=1
    while [ "$copy_attempt" -le 4 ]; do
        if scp -q -o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2 "$source_file" "$RVBOX_TEST_VBOX_HOST:$host_stage/$target_name"; then
            return 0
        fi
        copy_attempt=$((copy_attempt + 1))
        if [ "$copy_attempt" -le 4 ]; then
            printf 'stage: small-file SSH copy retry %s/4 (%s)\n' "$copy_attempt" "$target_name" >&2
            sleep 2
        fi
    done
    fail "could not copy staged $target_name after 4 SSH attempts"
}

case $action in
    prepare)
        remote prepare
        printf 'prepared vm=%s stage=%s\n' "$RVBOX_TEST_VBOX_VM" "$host_stage"
        ;;
    stage)
        printf 'stage: verify prepared VM and lease\n'
        remote prepare-stage
        [ -f "$bundle/rvbox.exe" ] && [ ! -L "$bundle/rvbox.exe" ] || fail "rvbox.exe must be a regular non-symlink file"
        [ -f "$bundle/client.toml" ] && [ ! -L "$bundle/client.toml" ] || fail "client.toml must be a regular non-symlink file"
        if [ -f "$bundle/ca.pem" ]; then
            [ ! -L "$bundle/ca.pem" ] || fail "ca.pem must not be a symlink"
            local_hashes=$(cd "$bundle" && sha256sum rvbox.exe client.toml ca.pem)
        else
            local_hashes=$(cd "$bundle" && sha256sum rvbox.exe client.toml)
        fi
        copy_stage_file "$bundle/client.toml" client.toml
        if [ -f "$bundle/ca.pem" ]; then copy_stage_file "$bundle/ca.pem" ca.pem; fi
        local_exe_hash=$(sha256sum "$bundle/rvbox.exe" | awk '{print $1}')
        remote_exe_hash=$(ssh -o BatchMode=yes "$RVBOX_TEST_VBOX_HOST" "sha256sum '$host_stage/rvbox.exe' 2>/dev/null | awk '{print \$1}'" 2>/dev/null || true)
        if [ "$local_exe_hash" = "$remote_exe_hash" ]; then
            printf 'stage: matching accelerated executable already present\n'
        else
            accelerated_stage || fail "accelerated executable transfer failed"
            trap 'if [ -n "$accelerated_artifact" ]; then rm -f "$accelerated_artifact"; fi' EXIT HUP INT TERM
        fi
        if [ -f "$bundle/ca.pem" ]; then
            remote_hashes=$(ssh -o BatchMode=yes "$RVBOX_TEST_VBOX_HOST" "cd '$host_stage' && sha256sum rvbox.exe client.toml ca.pem" 2>/dev/null || true)
        else
            remote_hashes=$(ssh -o BatchMode=yes "$RVBOX_TEST_VBOX_HOST" "cd '$host_stage' && sha256sum rvbox.exe client.toml" 2>/dev/null || true)
        fi
        [ "$local_hashes" = "$remote_hashes" ] || fail "bundle transfer did not reach the expected SHA-256 manifest"
        printf 'verified transfer_sha256 run_id=%s\n%s\n' "$run_id" "$local_hashes"
        remote stage
        remote stage-create-root
        remote stage-copy-exe
        remote stage-copy-config
        if [ -f "$bundle/ca.pem" ]; then remote stage-copy-ca; fi
        if [ -n "$accelerated_artifact" ]; then
            rm -f "$accelerated_artifact"
            accelerated_artifact=
            trap - EXIT HUP INT TERM
        fi
        printf 'staged guest_root=%s\n' "$guest_root"
        ;;
    collect)
        remote collect
        local_artifacts=$run_root/$run_id/artifacts
        mkdir -p "$local_artifacts"
        scp -q -r "$RVBOX_TEST_VBOX_HOST:$host_stage/artifacts/." "$local_artifacts/" 2>/dev/null || true
        printf 'artifacts=%s\n' "$local_artifacts"
        ;;
    *) remote "$action" ;;
esac
