#!/bin/sh
# The Phase 0 gate, guest side.
#
# Answers one question with an exit code: is a Windows shell actually running
# on this machine, and can it launch and show a Win32 app? Printed output is
# for a human reading CI logs; the exit code is what the gate consumes.
#
#   0  all checks passed
#   1  a check failed
#   2  the session never came up (nothing to check)
set -u

SG_LIB="${SG_LIB:-/usr/lib/stained-glass}"
# shellcheck source=lib/sg-common.sh
. "$SG_LIB/sg-common.sh"

TIMEOUT="${SG_CHECK_TIMEOUT:-120}"
FAILED=0

pass() { echo "PASS  $*"; }
fail() { echo "FAIL  $*"; FAILED=1; }
info() { echo "info  $*"; }

# --- wait for the session to publish itself -------------------------------
waited=0
while [ ! -f "$(sg_session_env)" ]; do
    [ "$waited" -ge "$TIMEOUT" ] && { echo "FAIL  session never started (no $(sg_session_env) after ${TIMEOUT}s)"; exit 2; }
    sleep 2
    waited=$((waited + 2))
done
# shellcheck source=/dev/null  # written by sg-run-explorer at session start
. "$(sg_session_env)"
export DISPLAY XDG_RUNTIME_DIR WAYLAND_DISPLAY WINEPREFIX
info "session.env found after ${waited}s (display=${DISPLAY:-none} path=${SG_DISPLAY_PATH:-?})"

sg_wine_env

# --- 1. the Wine system is alive ------------------------------------------
# wineserver is the kernel of the Wine personality. If it is not running,
# nothing else below means anything.
waited=0
# Multi-user debt D10: match only processes that belong to this check.
# explorer and notepad must be *this user's*; the wineserver is the machine's,
# run by the SYSTEM account (the image) -- or this user's own where there is no
# machine-level server (a developer box or CI). Never another user's.
_me=$(id -u)
_srv_owners="$_me"
getent passwd "${SG_SYSTEM_USER:-sgsystem}" >/dev/null 2>&1 && \
    _srv_owners="$_me,$(id -u "${SG_SYSTEM_USER:-sgsystem}")"
while ! pgrep -u "$_srv_owners" -x wineserver >/dev/null 2>&1; do
    [ "$waited" -ge "$TIMEOUT" ] && break
    sleep 2
    waited=$((waited + 2))
done
if pgrep -u "$_srv_owners" -x wineserver >/dev/null 2>&1; then
    pass "wineserver is running"
else
    fail "wineserver is not running"
fi

# explorer must be up as the session's shell. Do not identify it by the
# command line `explorer.exe /desktop=shell,WxH`: that process is not reliably
# there. On the image it has been seen gone entirely, with the "shell" desktop
# owned by a process whose command line reads just `explorer.exe /desktop`;
# under stock Wine both processes are present. Why they differ is not yet
# established. Matching the command line made this gate pass or fail depending
# on which of those states it caught. So assert explorer is running here; that
# it is *our* named "shell" desktop is proven by its window in section 2, and
# that no shell leaked into session 0 is guarded just below -- neither depends
# on a command line.
waited=0
while ! pgrep -u "$_me" -f 'explorer\.exe' >/dev/null 2>&1; do
    [ "$waited" -ge "$TIMEOUT" ] && break
    sleep 2
    waited=$((waited + 2))
done
if pgrep -u "$_me" -f 'explorer\.exe' >/dev/null 2>&1; then
    pass "explorer.exe is running"
else
    fail "explorer.exe is not running"
fi

# Linux programs the shell starts (Start, SG Store's Run, the Run box) open
# on X11, where the taskbar frames them: their environment -- the shell's --
# says the session type is x11. wlroots sets it to wayland in the compositor,
# and Electron 39+ went native Wayland by it, over the taskbar with no button
# (the Claude desktop app, David 2026-10-06; sg-common.sh sg_linux_app_env).
_xp=$(pgrep -u "$_me" -f 'explorer\.exe' | head -1)
if [ -n "$_xp" ] && [ -r "/proc/$_xp/environ" ]; then
    _st=$(tr '\0' '\n' < "/proc/$_xp/environ" | sed -n 's/^XDG_SESSION_TYPE=//p')
    if [ "$_st" = x11 ]; then
        pass "the shell's programs are told the session is x11 (Electron opens on X11)"
    else
        fail "the shell's programs see XDG_SESSION_TYPE='$_st': Electron 39+ opens native Wayland, over the taskbar"
    fi
fi

# The interactive shell belongs on WinSta0 as the session user, never in the
# service session (session 0). wine-sg patch 0007 refuses a shell there; this
# guards that it held. A leaked shell would run under the account that hosts the
# machine wineserver ($SG_SYSTEM_USER), so check ownership -- durable where the
# command line is not. Skipped when that account is us or absent, as on the
# single-user dev gate, where there is no separate service session to leak into.
_sysuser="${SG_SYSTEM_USER:-sgsystem}"
if id "$_sysuser" >/dev/null 2>&1 && [ "$_sysuser" != "$(id -un)" ]; then
    # explorer.exe as a program, not as text: the printer helpers' command
    # lines carry WINEDLLOVERRIDES=...;explorer.exe=d, which matched and
    # failed boot-tests now and then (2026-10-06)
    if pgrep -u "$_sysuser" -f 'explorer\.exe( |$)' >/dev/null 2>&1; then
        fail "an explorer shell is running as $_sysuser -- a shell leaked into session 0"
    else
        pass "no interactive shell leaked into the service session"
    fi
fi

# The session must have published a usable display. Treat an empty one as an
# explicit failure: under `set -u` it would otherwise surface as a bare
# "DISPLAY: parameter not set" from the shell, which says nothing about what
# actually went wrong.
if [ "${SG_DISPLAY_PATH:-x11}" = "x11" ] && [ -z "${DISPLAY:-}" ]; then
    fail "the session published an empty DISPLAY -- XWayland had not started when the session was written"
    echo; echo "RESULT: FAIL"
    exit 1
fi

# --- 2. the desktop window exists -----------------------------------------
# Only the X11 path can be checked this way. cage exposes no toplevel
# enumeration protocol, so on the Wayland path there is nothing to query --
# which is itself the reason Phase 0 ships the X11 path (ADR 0003).
if [ "${SG_DISPLAY_PATH:-x11}" != "x11" ]; then
    fail "display path is '${SG_DISPLAY_PATH}': window checks are only implemented for x11"
    echo; echo "RESULT: $([ "$FAILED" -eq 0 ] && echo PASS || echo FAIL)"
    exit "$FAILED"
fi

window_list() { xwininfo -root -children 2>/dev/null; }

# Read a PE file's machine type without needing file(1), which a minimal image
# does not ship. A PE has a 4-byte little-endian offset to its PE header at
# 0x3c; that header is "PE\0\0" followed by a 2-byte machine type.
# 0x014c is i386, 0x8664 is x86-64.
pe_machine() {
    _off=$(od -An -tu4 -j60 -N4 "$1" 2>/dev/null | tr -d ' ')
    [ -n "$_off" ] || return 1
    od -An -tx2 -j "$((_off + 4))" -N2 "$1" 2>/dev/null | tr -d ' \n'
}

# Windows drawn inside the Wine desktop are children of the desktop's X window,
# not top-level X windows. Enumerating the root only shows the desktop itself,
# so anything that looks for an app has to look one level down.
# Matched case-insensitively on purpose: Wine titles this window
# "shell - Wine Desktop" in Debian's build and "shell - Wine desktop" in
# Ubuntu's. Matching one spelling passes on one distribution and fails on the
# other, which is how CI found this.
desktop_id() { window_list | grep -i 'shell - wine desktop' | awk '{print $1}' | head -1; }
desktop_children() {
    id=$(desktop_id)
    [ -n "$id" ] && xwininfo -id "$id" -children 2>/dev/null
}

waited=0
while ! window_list | grep -qi 'shell - wine desktop'; do
    [ "$waited" -ge "$TIMEOUT" ] && break
    sleep 2
    waited=$((waited + 2))
done
if window_list | grep -qi 'shell - wine desktop'; then
    DESKTOP_LINE=$(window_list | grep -i 'shell - wine desktop' | sed 's/^ *//' | head -1)
    pass "desktop window exists: $DESKTOP_LINE"
else
    fail "no Wine desktop window on $DISPLAY"
    info "window list was:"; window_list | sed 's/^/      /' | head -20
    echo; echo "RESULT: FAIL"
    exit 1
fi

# --- 3. the desktop has a taskbar -----------------------------------------
# The brief's Phase 0 target is a taskbar with a running app in it, so the gate
# checks for one rather than taking the desktop window as proof.
#
# Wine's taskbar is an unnamed child of the desktop spanning its full width, a
# few tens of pixels tall, sitting at the bottom. Identify it by that shape:
# there is no window name or class to match on.
DESKTOP_W=$(echo "$DESKTOP_LINE" | sed -n 's/.*  \([0-9]\+\)x[0-9]\+[+-].*/\1/p')
#
# Kept to POSIX awk deliberately: gawk's three-argument match() is not in mawk,
# which is what a Debian image actually ships.
taskbar_line() {
    desktop_children | awk -v w="$DESKTOP_W" '
        NF >= 2 {
            geom = $(NF - 1)                    # e.g. 1280x20+0+700
            if (split(geom, a, "x") != 2) next
            if (split(a[2], b, "+") != 3) next
            width = a[1]; height = b[1]; x = b[2]
            if (width == w && height <= 64 && x == 0) { print; exit }
        }'
}
waited=0
while [ -z "$(taskbar_line)" ]; do
    [ "$waited" -ge "$TIMEOUT" ] && break
    sleep 2
    waited=$((waited + 2))
done
if [ -n "$(taskbar_line)" ]; then
    pass "taskbar present: $(taskbar_line | sed 's/^ *//')"
else
    fail "no taskbar found among the desktop's children"
    info "desktop children were:"; desktop_children | sed 's/^/      /' | head -20
fi

# Clear any app left over from a previous run. Doing this at the start rather
# than the end keeps the check idempotent *and* leaves the last app on screen,
# so the boot gate's screenshot catches a live window instead of one being torn
# down. Without it, a leftover Notepad from an earlier run would satisfy the
# checks below and the gate would pass without launching anything.
pkill -f 'notepad.exe' >/dev/null 2>&1 || true

# --- 4. a Win32 app launches and shows up in the desktop ------------------
# notepad is the canary: a stock Win32 app with a titled top-level window.
# It must appear as a child of the desktop, not as its own X toplevel -- an app
# outside the desktop is an app that is not in the taskbar, and under a
# fullscreen compositor it covers the shell entirely.
info "launching notepad.exe"
# Private to this user (debt D11): not the shared log directory.
_np_out=$(mktemp); _np32_out=$(mktemp)
trap 'rm -f "$_np_out" "$_np32_out"' EXIT
wine notepad >"$_np_out" 2>&1 &
NOTEPAD_SHELL_PID=$!

waited=0
while ! desktop_children | grep -q 'Notepad'; do
    [ "$waited" -ge "$TIMEOUT" ] && break
    sleep 2
    waited=$((waited + 2))
done
if desktop_children | grep -q 'Notepad'; then
    pass "notepad is inside the desktop: $(desktop_children | grep 'Notepad' | sed 's/^ *//' | head -1)"
else
    fail "notepad did not appear inside the desktop after ${TIMEOUT}s"
    if window_list | grep -q 'Notepad'; then
        fail "  (it opened as its own X toplevel -- the virtual desktop is not the prefix default)"
    fi
    info "notepad output:"; sed 's/^/      /' "$_np_out" 2>/dev/null | head -20
    info "desktop children were:"; desktop_children | sed 's/^/      /' | head -20
fi

if pgrep -u "$_me" -f 'notepad.exe' >/dev/null 2>&1; then
    pass "notepad.exe process is alive"
else
    fail "notepad.exe process is not running"
fi

pkill -f 'notepad.exe' >/dev/null 2>&1 || true
kill "$NOTEPAD_SHELL_PID" >/dev/null 2>&1 || true

# --- 5. a 32-bit Windows app runs in the shell ----------------------------
# The point of shipping wine-sg rather than a distribution Wine is that this
# machine runs 32-bit Windows applications without i386 multiarch. Proving the
# Wine build can do it is wine-sg's job; proving it works *in a real session*,
# rendered inside the shell, is this gate's.
#
# Skipped rather than failed when there is no syswow64, so the gate still runs
# meaningfully against a distribution Wine on a developer box.
WOW_NOTEPAD="$SG_PREFIX/drive_c/windows/syswow64/notepad.exe"
if [ ! -f "$WOW_NOTEPAD" ]; then
    info "SKIP 32-bit check: no syswow64 in the prefix (not a new-WoW64 Wine)"
else
    WOW_MACHINE=$(pe_machine "$WOW_NOTEPAD")
    case "$WOW_MACHINE" in
        014c) pass "syswow64/notepad.exe is a 32-bit PE (machine 0x$WOW_MACHINE, i386)" ;;
        8664) fail "syswow64/notepad.exe is x86-64 (machine 0x$WOW_MACHINE) -- syswow64 should hold 32-bit binaries" ;;
        "")   fail "could not read a PE header from syswow64/notepad.exe" ;;
        *)    fail "syswow64/notepad.exe has unexpected machine type 0x$WOW_MACHINE" ;;
    esac

    info "launching the 32-bit notepad.exe"
    wine 'C:\windows\syswow64\notepad.exe' >"$_np32_out" 2>&1 &

    waited=0
    while ! desktop_children | grep -q 'Notepad'; do
        [ "$waited" -ge "$TIMEOUT" ] && break
        sleep 2
        waited=$((waited + 2))
    done
    if desktop_children | grep -q 'Notepad'; then
        pass "32-bit app runs in the shell: $(desktop_children | grep 'Notepad' | sed 's/^ *//' | head -1)"
    else
        fail "the 32-bit notepad did not appear inside the desktop after ${TIMEOUT}s"
        info "output:"; sed 's/^/      /' "$_np32_out" 2>/dev/null | head -20
    fi

    # Deliberately left running: the boot gate screenshots immediately after
    # this returns, and the whole point of the screenshot is to show a real
    # application in the shell.
fi

echo
if [ "$FAILED" -eq 0 ]; then
    echo "RESULT: PASS"
else
    echo "RESULT: FAIL"
fi
exit "$FAILED"
