#!/bin/bash
#
# Type at the SDL window with a real keyboard, and check what comes out.
#
# This is the only test in the repository that exercises the path a person
# actually uses: X11 delivers a key press to a focused window, SDL composes it,
# libakgl rings it, the line editor echoes it, and the interpreter runs it. Every
# other keyboard test synthesises SDL events, which is a fine way to test the
# code downstream of SDL and no way at all to test the code upstream of it.
#
# **It exists because that gap shipped a bug.** The frontend never called
# SDL_StartTextInput(), so SDL emitted no text-input events, every keystroke
# arrived with an empty `text`, and the editor dropped the lot -- no echo, and
# nothing on stdout either, because RUN could never be typed. The suite was
# green throughout, because it pushed the text-input events itself.
#
# Skips rather than fails when it cannot run: no display, no xdotool, no
# script(1), no window, or no focus are all "this machine cannot answer the
# question", not "the answer is no". CTest is told exit 77 means skipped.
#
# **It steals keyboard focus for a few seconds.** That is inherent -- the point
# is a focused window. Set AKBASIC_SKIP_INTERACTIVE=1 to skip it while you work.

set -u

SKIP=77
BASIC="${1:?usage: akgl_typing.sh <path to basic>}"
WORK="$(mktemp -d)"
trap 'rm -rf "${WORK}"' EXIT

skip() { echo "SKIP: $*"; exit "${SKIP}"; }
fail() { echo "FAIL: $*"; exit 1; }

# ---------------------------------------------------------------- environment
[ "${AKBASIC_SKIP_INTERACTIVE:-0}" = "1" ] && skip "AKBASIC_SKIP_INTERACTIVE=1"
[ -n "${DISPLAY:-}" ] || skip "no DISPLAY; this needs a real X server, not the dummy driver"
command -v xdotool >/dev/null 2>&1 || skip "xdotool is not installed"
command -v script  >/dev/null 2>&1 || skip "script(1) is not installed"
xdotool getactivewindow >/dev/null 2>&1 || skip "no window manager is answering on ${DISPLAY}"

# script(1) gives the driver a pty for stdin, which is what makes it choose the
# *window* as its console. Redirected stdin is a deliberate signal to read the
# pipe instead -- see run_akgl() in src/main.c -- and under CTest stdin is never
# a terminal, so without this the driver would ignore the keyboard entirely and
# the test would be measuring nothing.
OUT="${WORK}/typescript"
script -qefc "${BASIC}" "${OUT}" >/dev/null 2>&1 &
DRIVER=$!

# shellcheck disable=SC2317  # reached only through the EXIT trap below
cleanup_driver() {
    kill "${DRIVER}" 2>/dev/null
    wait "${DRIVER}" 2>/dev/null
}
trap 'cleanup_driver; rm -rf "${WORK}"' EXIT

# ------------------------------------------------------------------ the window
# --sync blocks until it appears rather than sleeping and hoping.
WID="$(timeout 20 xdotool search --sync --onlyvisible --name '^BASIC$' 2>/dev/null | head -1)"
[ -n "${WID}" ] || skip "the BASIC window never appeared (headless, or a WM that does not map it)"

xdotool windowactivate --sync "${WID}" >/dev/null 2>&1
xdotool windowfocus    --sync "${WID}" >/dev/null 2>&1
FOCUS="$(xdotool getwindowfocus 2>/dev/null)"
[ "${FOCUS}" = "${WID}" ] || skip "could not give the window keyboard focus (got '${FOCUS}', wanted '${WID}')"

# ------------------------------------------------------------------- the typing
# Poll for expected output instead of sleeping a guessed interval: a fixed sleep
# is either flaky or slow, and usually both.
await() {
    local want="$1" limit="${2:-15}" i=0
    while [ "${i}" -lt $((limit * 10)) ]; do
        if tr -d '\r' < "${OUT}" 2>/dev/null | grep -qF "${want}"; then
            return 0
        fi
        sleep 0.1
        i=$((i + 1))
    done
    return 1
}

type_line() {
    xdotool type --clearmodifiers --delay 30 "$1"
    xdotool key  --clearmodifiers Return
}

await "READY" 20 || skip "the interpreter never reached its prompt"

# A string literal on purpose: the double quote is the character that was
# unreachable before the keystroke ring carried composed text, and a BASIC
# program that cannot contain a string is not much of a BASIC.
type_line '10 PRINT "TYPED OK"'
type_line 'RUN'

if ! await "TYPED OK" 15; then
    echo "--- what the driver actually wrote ---"
    tr -d '\r' < "${OUT}" 2>/dev/null | sed -n '1,20p'
    fail "typing at the window produced nothing: the keyboard path is broken"
fi

# Lower case has to survive too. It could not before libakgl 0.3.0, because a
# keycode cannot express shift and the editor folded everything to upper case.
type_line '20 PRINT "lower"'
type_line 'RUN'
if ! await "lower" 15; then
    echo "--- what the driver actually wrote ---"
    tr -d '\r' < "${OUT}" 2>/dev/null | sed -n '1,20p'
    fail "lower case did not survive the keyboard: composed text is not reaching the editor"
fi

# Backspace, end to end and through what it produces rather than what it draws.
# Typing PRINX, rubbing out the X and finishing the word only runs if backspace
# actually removed a character from the line buffer -- otherwise this is a
# syntax error. Reported as "the backspace key does not clear the character
# underneath it".
type_line '30 PRINX'$'\b''T "BS OK"'
type_line 'RUN'
if ! await "BS OK" 15; then
    echo "--- what the driver actually wrote ---"
    tr -d '\r' < "${OUT}" 2>/dev/null | sed -n '1,30p'
    fail "backspace did not remove a character: the line reached the parser unedited"
fi

# More lines than the window has rows, so the text has to scroll. Checked
# through stdout, which proves the interpreter kept running across the scroll;
# that the *screen* scrolls correctly is asserted at pixel level in
# tests/akgl_frontend.c, which can read the render target back and this cannot.
type_line '40 FOR I# = 1 TO 40'
type_line '50 PRINT "SCROLL"'
type_line '60 NEXT I#'
type_line 'RUN 40'
if ! await "SCROLL" 20; then
    echo "--- what the driver actually wrote ---"
    tr -d '\r' < "${OUT}" 2>/dev/null | sed -n '1,30p'
    fail "a program long enough to scroll the window produced nothing"
fi
COUNT="$(tr -d '\r' < "${OUT}" | grep -c '^SCROLL$')"
[ "${COUNT}" -eq 40 ] || fail "expected 40 scrolled lines, got ${COUNT}"

type_line 'QUIT'
await "" 1 >/dev/null 2>&1 || true

echo "PASS: typed at a real focused window; backspace, scrolling and RUN all worked"
exit 0
