#!/bin/bash
#
# klammerset_test.sh — Regression tests for the @@@klammerset system command.
#
# @@@klammerset declares a Klammerset: a named, logically related group of
# klammer definitions.  The declaration is operative — processing it reads
# the :requires files and then the :files, in list order, at the point of the
# declaration; relative names resolve against the declaring file's directory,
# never the cwd.  A repeated declaration of an already-registered symbol is
# skipped (loaded once), which is what makes :requires idempotent.  The
# klammers themselves live in the Machine's flat Klammer_registry; the
# Klammerset holds metadata and the file list only.
#
# Engine tier: no SKS.  Fixtures live in tst/klammerset/ and define their own
# target ("fix") inline.
#
# Usage: ./klammerset_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}
FIX=$K/tst/klammerset

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

# strip leading/trailing blank lines and surrounding whitespace
trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR && line[f]=="") f++; l=NR; while (l>=1 && line[l]=="") l--; for (i=f;i<=l;i++) print line[i] }'; }

# check_eq TEST_NAME EXPECTED KTEXT_ARGS...
#   Runs ktext, expects exit status 0, and compares trimmed stdout to EXPECTED.
check_eq() {
    local test_name="$1"
    local expected="$2"
    shift 2

    local output status
    output=$("$KTEXT" "$@" 2>/tmp/klammerset_test_err.$$)
    status=$?
    output=$(printf '%s' "$output" | trim)

    if [ $status -ne 0 ]; then
        echo "${red}FAIL${reset} $test_name — ktext exited $status"
        echo "       stderr: $(head -3 /tmp/klammerset_test_err.$$)"
        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
}

# check_contains TEST_NAME SUBSTRING KTEXT_ARGS...
#   Runs ktext, expects exit status 0, and checks that the combined output
#   contains SUBSTRING.
check_contains() {
    local test_name="$1"
    local needle="$2"
    shift 2

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

    if [ $status -ne 0 ]; then
        echo "${red}FAIL${reset} $test_name — ktext exited $status"
        echo "       output: $(echo "$output" | head -3)"
        FAIL=$((FAIL + 1))
        return
    fi
    if printf '%s' "$output" | grep -qF "$needle"; then
        echo "${green}PASS${reset} $test_name"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $test_name"
        echo "       expected output to contain: [$needle]"
        echo "       got: $(echo "$output" | head -5)"
        FAIL=$((FAIL + 1))
    fi
}

# check_fails TEST_NAME SUBSTRING KTEXT_ARGS...
#   Runs ktext, expects a NONZERO exit status, and checks that the combined
#   output contains SUBSTRING.
check_fails() {
    local test_name="$1"
    local needle="$2"
    shift 2

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

    if [ $status -eq 0 ]; then
        echo "${red}FAIL${reset} $test_name — expected an error, ktext exited 0"
        FAIL=$((FAIL + 1))
        return
    fi
    if printf '%s' "$output" | grep -qF "$needle"; then
        echo "${green}PASS${reset} $test_name"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $test_name"
        echo "       expected error output to contain: [$needle]"
        echo "       got: $(echo "$output" | head -5)"
        FAIL=$((FAIL + 1))
    fi
}

echo "${bold}@@@klammerset tests${reset}"
echo "======================="
echo

# --- Loading ---------------------------------------------------------------

# 1. :files load in list order: klammers.k defines @greet for the target
#    that base.k declares, so base.k must have been read first.
check_eq "1. :files load in order (target before klammer)" \
    "Hello World" \
    -k "$FIX/decl.k" -s '@greet World @' -t fix

# 2. :requires loads the dependency before the set's own files.
check_eq "2. :requires loads the dependency" \
    "--" \
    -k "$FIX/decl.k" -s '@dash@' -t fix

# 3. Program order: a definition AFTER the declaration in the declaring file
#    is available (there is no :text argument; the declaring file's own
#    content plays that role).
check_eq "3. trailing definition in the declaring file" \
    "AFTER" \
    -k "$FIX/decl.k" -s '@after@' -t fix

# 4. Relative :files names resolve against the DECLARING file's directory,
#    not the cwd (run from an unrelated directory).
output=$( (cd /tmp && "$KTEXT" -k "$FIX/decl.k" -s '@greet Elsewhere @' -t fix) 2>/dev/null | trim )
if [ "$output" = "Hello Elsewhere" ]; then
    echo "${green}PASS${reset} 4. :files resolve against the declaring file's directory"
    PASS=$((PASS + 1))
else
    echo "${red}FAIL${reset} 4. :files resolve against the declaring file's directory"
    echo "       expected: [Hello Elsewhere]"
    echo "       got:      [$output]"
    FAIL=$((FAIL + 1))
fi

# 5. A filename with a space in the :files list (standalone "/" separator).
check_eq "5. spacey filename in :files" \
    "SPACEY" \
    -k "$FIX/spacey.k" -s '@spacey@' -t fix

# --- The already-loaded guard ------------------------------------------------

# 6. A second declaration of an already-registered symbol is skipped, not an
#    error: wrapper.k requires decl.k (registers "kit") and then decl2.k
#    (re-declares "kit"); the wrapper still loads and kit's klammers work.
check_eq "6. duplicate declaration is skipped, not an error" \
    "Hello Again" \
    -k "$FIX/wrapper.k" -s '@greet Again @' -t fix

# 7. ...and the skipped declaration's files are NOT loaded.
check_fails "7. skipped declaration loads none of its files" \
    "only_dup" \
    -k "$FIX/wrapper.k" -s '@only_dup@' -t fix

# --- Introspection -----------------------------------------------------------

# 8. -m lists the registered klammersets.
check_contains "8. -m shows the klammerset symbol" \
    "kit" \
    -k "$FIX/decl.k" -s 'x' -t fix -m

check_contains "9. -m shows the required klammerset too" \
    "Utility klammers for engine tests" \
    -k "$FIX/decl.k" -s 'x' -t fix -m

# --- Errors --------------------------------------------------------------------

# 10. A listed file that does not exist is a clean klammerset error.
check_fails "10. missing file in :files" \
    "missing.k" \
    -k "$FIX/bad_file.k" -s 'x' -t fix

# 11. A symbol must be an identifier (starts with a letter; letters, digits,
#     underscores).
check_fails "11. invalid symbol rejected" \
    "not valid" \
    -k none -s '@@@klammerset 9bad | Bad symbol @@@' -d

# --- The search path (symbol -> <dir>/<symbol>/<symbol>.k) -------------------

# Runtime fixtures: a document directory holding a local klammerset, and a
# separate directory serving as a KLAMMERTEXT_KLAMMERSETS stage.
DOCDIR=$(mktemp -d /tmp/klammerset_doc.XXXXXX)
ENVDIR=$(mktemp -d /tmp/klammerset_env.XXXXXX)
trap 'rm -rf "$DOCDIR" "$ENVDIR" /tmp/klammerset_test_err.$$' EXIT

mkdir -p "$DOCDIR/locset" "$ENVDIR/envset" "$ENVDIR/locset"
printf '@@@klammerset locset | Document-local set @@@\n@@@target fixL | Fixture @@@\n@@local_k.fixL : LOCAL @@\n' \
    > "$DOCDIR/locset/locset.k"
printf '@local_k@\n' > "$DOCDIR/doc.kt"
printf '@@@klammerset envset | Installed set @@@\n@@@target fixE | Fixture @@@\n@@env_k.fixE : ENV @@\n' \
    > "$ENVDIR/envset/envset.k"
printf '@@@klammerset locset | Shadow candidate @@@\n@@@target fixL | Fixture @@@\n@@local_k.fixL : ENV-SHADOWED @@\n' \
    > "$ENVDIR/locset/locset.k"

# 12. Stage 1 for a file input: the document's directory.
check_eq "12. symbol resolves in the document's directory" \
    "LOCAL" \
    "$DOCDIR/doc.kt" -k locset -t fixL -d

# 13. Stage 1 for string input: the cwd stands in for the document.
output=$( (cd "$DOCDIR" && "$KTEXT" -s '@local_k@' -k locset -t fixL) 2>/dev/null | trim )
if [ "$output" = "LOCAL" ]; then
    echo "${green}PASS${reset} 13. cwd stands in for the document (string input)"
    PASS=$((PASS + 1))
else
    echo "${red}FAIL${reset} 13. cwd stands in for the document (string input)"
    echo "       expected: [LOCAL]  got: [$output]"
    FAIL=$((FAIL + 1))
fi

# 14. Stage 2: the KLAMMERTEXT_KLAMMERSETS directories.
output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" -s '@env_k@' -k envset -t fixE 2>/dev/null | trim)
if [ "$output" = "ENV" ]; then
    echo "${green}PASS${reset} 14. symbol resolves in KLAMMERTEXT_KLAMMERSETS"
    PASS=$((PASS + 1))
else
    echo "${red}FAIL${reset} 14. symbol resolves in KLAMMERTEXT_KLAMMERSETS"
    echo "       expected: [ENV]  got: [$output]"
    FAIL=$((FAIL + 1))
fi

# 15. Shadowing: the document-local set wins over the installed one.
output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" "$DOCDIR/doc.kt" -k locset -t fixL -d 2>/dev/null | trim)
if [ "$output" = "LOCAL" ]; then
    echo "${green}PASS${reset} 15. document-local set shadows the installed one"
    PASS=$((PASS + 1))
else
    echo "${red}FAIL${reset} 15. document-local set shadows the installed one"
    echo "       expected: [LOCAL]  got: [$output]"
    FAIL=$((FAIL + 1))
fi

# 16. :requires by symbol, resolved with the declaring directory as the
#     local stage (the required set sits inside the declaring set's dir).
mkdir -p "$DOCDIR/kit2/locset"
cp "$DOCDIR/locset/locset.k" "$DOCDIR/kit2/locset/locset.k"
printf '@@@klammerset kit2 | Requires by symbol :requires locset @@@\n' > "$DOCDIR/kit2/kit2.k"
check_eq "16. :requires accepts a symbol (declaring-dir stage)" \
    "LOCAL" \
    -k "$DOCDIR/kit2/kit2.k" -s '@local_k@' -t fixL

# 17. Stage 3: $KLAMMERTEXT_HOME — the kdesc listing enumerates sks
#     (sks/sks.k already satisfies the <symbol>/<symbol>.k convention).
output=$(kdesc --klammerset 2>/dev/null)
if printf '%s' "$output" | grep -q 'sks/sks\.k'; then
    echo "${green}PASS${reset} 17. kdesc --klammerset lists sks from KLAMMERTEXT_HOME"
    PASS=$((PASS + 1))
else
    echo "${red}FAIL${reset} 17. kdesc --klammerset lists sks from KLAMMERTEXT_HOME"
    echo "       got: $(echo "$output" | head -3)"
    FAIL=$((FAIL + 1))
fi

# 18. An unknown symbol is a clean error naming the search directories.
check_fails "18. unknown symbol names the search path" \
    "was not found" \
    -s 'x' -k nosuchset -d

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