#!/bin/bash
#
# recursion_test.sh — The klammer application recursion guard.
#
# Before the guard, a klammer that applied itself -- directly or through a
# cycle -- descended until the C++ stack was exhausted.  The process died with
# SIGSEGV: no message, no location, no indication of which klammer was at
# fault, and a core dump.  For a language whose premise is that users define
# their own klammers, that was the worst available failure mode.
#
# Machine::apply_klammer() now carries a depth guard (Depth_guard in
# mac/machine.cpp) that raises a Recursion_error naming the klammer and its
# location.  Separately, the top-level fixed-point loop in Machine::apply()
# ends when a pass applies no klammer -- rather than when the katom list stops
# growing -- and exceeding its round limit is an error rather than a message
# followed by rendering a document with live klammers still in it.
#
# These are engine tests: no klammer set is loaded (--klammersets none) and every klammer
# used is defined inline as a fixture.
#
# Usage: ./recursion_test.sh
# Exit code: 0 if all tests pass, 1 otherwise.

PASS=0
FAIL=0
KTEXT=ktext
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}

red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'

# check_error TEST_NAME PATTERN KTEXT_ARGS...
#   Runs ktext, expects a NONZERO exit status and PATTERN in the message.
#   A signal death (exit >= 128) is called out separately: that is the exact
#   regression this suite exists to prevent, and reporting it as "some error"
#   would hide it.
check_error() {
    local test_name="$1"
    local pattern="$2"
    shift 2

    local output status
    output=$("$KTEXT" "$@" 2>&1)
    status=$?

    if [ $status -ge 128 ]; then
        echo "${red}FAIL${reset} $test_name — ktext died from signal $((status - 128))"
        FAIL=$((FAIL + 1))
        return
    fi
    if [ $status -eq 0 ]; then
        echo "${red}FAIL${reset} $test_name — expected an error but ktext succeeded"
        FAIL=$((FAIL + 1))
        return
    fi
    if echo "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
        echo "${green}PASS${reset} $test_name"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $test_name — expected error to contain [$pattern]"
        echo "       output: $(echo "$output" | head -4)"
        FAIL=$((FAIL + 1))
    fi
}

# check_eq TEST_NAME EXPECTED KTEXT_ARGS...
check_eq() {
    local test_name="$1"
    local expected="$2"
    shift 2

    local output status
    output=$("$KTEXT" "$@" 2>/dev/null)
    status=$?
    output=$(printf '%s' "$output" | tr -d '\n' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')

    if [ $status -ne 0 ]; then
        echo "${red}FAIL${reset} $test_name — ktext exited $status"
        FAIL=$((FAIL + 1))
        return
    fi
    if [ "$output" = "$expected" ]; then
        echo "${green}PASS${reset} $test_name"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $test_name"
        echo "       expected: [$expected]"
        echo "       got:      [$output]"
        FAIL=$((FAIL + 1))
    fi
}

echo "${bold}Klammer recursion guard tests${reset}"
echo "============================="
echo

# --- Non-termination is an error, not a crash ---

check_error " 1. direct self-recursion is caught" \
    "does not terminate" \
    --klammersets none -s '@@f : x @f@ @@ @f@' -d

check_error " 2. the offending klammer is named" \
    'applying "f"' \
    --klammersets none -s '@@f : x @f@ @@ @f@' -d

check_error " 3. mutual recursion is caught" \
    "does not terminate" \
    --klammersets none -s '@@a : ( @b@ ) @@ @@b : [ @a@ ] @@ @a@' -d

check_error " 4. self-recursion through an argument is caught" \
    "does not terminate" \
    --klammersets none -s '@@w t : < *t* > @@ @@r : @w @r@ @ @@ @r@' -d

# --- Terminating nesting is untouched ---

check_eq " 5. deep but finite nesting still reduces" \
    "<<<<<x>>>>>" \
    --klammersets none -s '@@w t : <*t*> @@ @w @w @w @w @w x @ @ @ @ @' -d

check_eq " 6. a chain of klammers generating klammers reduces" \
    "END" \
    --klammersets none -s '@@k1 : @k2@ @@ @@k2 : @k3@ @@ @@k3 : @k4@ @@ @@k4 : @k5@ @@ @@k5 : @k6@ @@ @@k6 : @k7@ @@ @@k7 : @k8@ @@ @@k8 : END @@ @k1@' -d

# --- The fixed point ends on "nothing was applied", not "nothing was added" ---
#
# A klammer whose body is empty reduces without adding katoms.  Under the old
# size-comparison test such a klammer looked like no progress at all.

check_eq " 7. a klammer with an empty body reduces" \
    "a  b" \
    --klammersets none -s '@@nothing : @@ a @nothing@ b' -d

echo
echo "============================="
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
[ $FAIL -eq 0 ]
