#!/usr/bin/env bash
# probeReplSmoke.sh - non-interactive smoke check for mameDebug.py
# --repl mode.  Pipes a canned script (`break main`, `run`, `where`,
# `quit`) into the REPL and asserts that:
#   1. The REPL parses each command without error
#   2. A breakpoint resolves through the link816 map
#   3. MAME launches with the bp installed and surfaces a BP-HIT line
#   4. `where` resolves the captured PC to a source line via DWARF
#
# Exit 0 on full pass.  Exit 77 (autotools "skip") if MAME / toolchain
# missing.  Exit 1 on any unexpected REPL output or missing capture.
#
# Usage: probeReplSmoke.sh [--verbose]

set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
VERBOSE=0
if [ "${1:-}" = "--verbose" ]; then
    VERBOSE=1
fi

CLANG="$ROOT/tools/llvm-mos-build/bin/clang"
LLVMMC="$ROOT/tools/llvm-mos-build/bin/llvm-mc"
LINK="$ROOT/tools/link816"

if [ ! -x "$CLANG" ] || [ ! -x "$LLVMMC" ] || [ ! -x "$LINK" ]; then
    echo "probeReplSmoke: missing toolchain (clang/llvm-mc/link816)" >&2
    exit 77
fi
if ! command -v mame >/dev/null 2>&1; then
    echo "probeReplSmoke: mame not on PATH; skipping" >&2
    exit 77
fi

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
CFILE="$WORK/repltest.c"
OFILE="$WORK/repltest.o"
OCRT0="$WORK/crt0.o"
OLIBGCC="$WORK/libgcc.o"
BIN="$WORK/repltest.bin"
MAP="$WORK/repltest.map"
DWARF="$WORK/repltest.dwarf"
OUT="$WORK/repl.out"

cat > "$CFILE" <<'EOF'
int gAnswer = 42;
int add(int a, int b) {
    int c = a + b;
    return c;
}
int main(void) {
    int r = add(3, 4);
    gAnswer = r;
    while (1) { }
    return r;
}
EOF

"$CLANG" --target=w65816 -O0 -g -ffunction-sections \
    -c "$CFILE" -o "$OFILE" 2>/dev/null
"$LLVMMC" -arch=w65816 -filetype=obj \
    "$ROOT/runtime/src/crt0.s" -o "$OCRT0" 2>/dev/null
"$LLVMMC" -arch=w65816 -filetype=obj \
    "$ROOT/runtime/src/libgcc.s" -o "$OLIBGCC" 2>/dev/null
"$LINK" -o "$BIN" --text-base 0x1000 \
    --map "$MAP" --debug-out "$DWARF" \
    "$OCRT0" "$OFILE" "$OLIBGCC" >/dev/null 2>&1 || true

[ -s "$BIN" ]   || { echo "probeReplSmoke: empty .bin"; exit 1; }
[ -s "$DWARF" ] || { echo "probeReplSmoke: empty DWARF sidecar"; exit 1; }
[ -s "$MAP" ]   || { echo "probeReplSmoke: empty map"; exit 1; }

# Phase 1: existing single-frame `bp main` smoke (kept to ensure the
# baseline path still works).  Then Phase 2: `bp add` + `--start-at
# main` to exercise the multi-frame `bt` walker.
printf 'break main\nrun\nwhere\nquit\n' \
    | timeout 60 python3 "$HERE/mameDebug.py" --repl \
        --bin "$BIN" --map "$MAP" --dwarf "$DWARF" \
        --seconds 4 > "$OUT" 2>&1 || {
    echo "probeReplSmoke: mameDebug.py --repl failed" >&2
    cat "$OUT" >&2
    exit 1
}

if [ "$VERBOSE" -eq 1 ]; then
    cat "$OUT" >&2
fi

# Required output lines:
#   "(dbg) break main"                  - command echo
#   "  bp #1 at 0x...... (main)"        - bp set ack
#   "(dbg) run"                         - command echo
#   "  PC=0x...... ... FUNC=main ..."   - where output after run
#   "(dbg) where"                       - command echo
#   "  PC=0x...... ... FUNC=main ..."   - where output (manual)
#   "(dbg) quit"                        - command echo
if ! grep -q "bp #1 at 0x" "$OUT"; then
    echo "probeReplSmoke: missing 'bp #1 at 0x...' breakpoint ack" >&2
    cat "$OUT" >&2
    exit 1
fi
if ! grep -q "FUNC=main" "$OUT"; then
    echo "probeReplSmoke: missing FUNC=main in 'where' output" >&2
    cat "$OUT" >&2
    exit 1
fi
# The `where` command (run AFTER the `run` command) must produce
# output too — verify by counting occurrences of "PC=0x" prefix lines.
PC_HITS=$(grep -c "^  PC=0x" "$OUT" || true)
if [ "$PC_HITS" -lt 2 ]; then
    echo "probeReplSmoke: expected >= 2 PC=0x lines (run + where), got $PC_HITS" >&2
    cat "$OUT" >&2
    exit 1
fi

# Bonus: verify the captured PC equals the map entry for `main`.
MAIN_PC=$(awk '$2 == "main" { print $1; exit }' "$MAP")
[ -n "$MAIN_PC" ] || { echo "probeReplSmoke: no 'main' symbol in map"; exit 1; }
MAIN_PC_LC=$(echo "$MAIN_PC" | tr 'A-Z' 'a-z')
if ! grep -qi "PC=$MAIN_PC_LC " "$OUT"; then
    echo "probeReplSmoke: captured PC does not match map[main]=$MAIN_PC" >&2
    cat "$OUT" >&2
    exit 1
fi

# Phase 2: multi-frame `bt` test.  Breakpoint at `add` with --start-at
# main: the JSL frame from main->add is live at the snapshot, so `bt`
# should walk back up at least one parent (>= 2 total frames).  This
# regression-checks both the .debug_frame_w65816 sidecar emit (link816)
# and the walker in mameDebug.py.
OUT2="$WORK/repl2.out"
printf 'break add\nrun\nbt\nquit\n' \
    | timeout 60 python3 "$HERE/mameDebug.py" --repl \
        --bin "$BIN" --map "$MAP" --dwarf "$DWARF" \
        --start-at main --seconds 4 > "$OUT2" 2>&1 || {
    echo "probeReplSmoke: mameDebug.py --repl (bt) failed" >&2
    cat "$OUT2" >&2
    exit 1
}

if [ "$VERBOSE" -eq 1 ]; then
    cat "$OUT2" >&2
fi

# Count frame lines (`  #N PC=0x...`) in the bt output.  Need >= 2 to
# prove the .debug_frame_w65816 sidecar drove a real parent-frame walk.
FRAME_LINES=$(grep -cE "^  #[0-9]+ PC=0x" "$OUT2" || true)
if [ "$FRAME_LINES" -lt 2 ]; then
    echo "probeReplSmoke: bt produced $FRAME_LINES frame lines (need >= 2)" >&2
    cat "$OUT2" >&2
    exit 1
fi

# Verify frame #0 is `add` and frame #1 is `main`.
if ! grep -q "^  #0 PC=0x.* FUNC=add " "$OUT2"; then
    echo "probeReplSmoke: bt frame #0 is not 'add'" >&2
    cat "$OUT2" >&2
    exit 1
fi
if ! grep -q "^  #1 PC=0x.* FUNC=main " "$OUT2"; then
    echo "probeReplSmoke: bt frame #1 is not 'main'" >&2
    cat "$OUT2" >&2
    exit 1
fi

echo "probeReplSmoke: OK (single-frame where + multi-frame bt OK)"
exit 0
