#!/bin/bash
#
# check_test.sh — Static checking of klammer applications ("kdiag --check").
#
# The engine applies klammers as it meets them, so it can only complain about
# what it reaches.  Two things it therefore never reaches:
#
#   * the branch of a @cond that is not selected.  @cond is a non-strict
#     special form, so an undefined klammer or a wrong argument count sitting
#     in the unselected branch is invisible -- and stays invisible until the
#     day the predicate flips.
#   * a klammer body that this render does not use, including a body defined
#     for a target other than the one being built.
#
# Klammertext can check both without running anything, because katom structure
# is fixed when a file is read: there are no catcodes, so nothing later can
# change how text already read divides into katoms.  Every application written
# literally in a document or a klammer body can be located and checked against
# the registry.  That is what check_machine() (mac/check.cpp) does and what
# these tests pin down.
#
# What the checker deliberately does not see is also tested: klammers produced
# by @eval at run time, and the interiors of @eval/@read argument spans and of
# literal parameters, which are code, filenames, and raw text -- not
# applications.
#
# Engine tier: kdiag loads no klammerset, so every klammer is defined inline.
# The one case that needs the SKS reads it explicitly with "@read" -- which is
# how a klammerset reaches kdiag at all: the symbol mechanism is deliberately
# absent there, so that kdiag still works when symbol resolution is what broke.
#
# Usage: ./check_test.sh
# Exit code: 0 if all tests pass, 1 otherwise.

PASS=0
FAIL=0
KDIAG=kdiag
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}

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

# check_finds TEST_NAME PATTERN KDIAG_ARGS...
#   --check must exit nonzero and report PATTERN.
check_finds() {
    local test_name="$1"
    local pattern="$2"
    shift 2

    local output status
    output=$("$KDIAG" --process --check "$@" 2>&1)
    status=$?

    if [ $status -eq 0 ]; then
        echo "${red}FAIL${reset} $test_name — expected a diagnostic, got none"
        FAIL=$((FAIL + 1))
        return
    fi
    if echo "$output" | grep -qF "$pattern"; then
        echo "${green}PASS${reset} $test_name"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $test_name — expected report to contain [$pattern]"
        echo "       output: $(echo "$output" | head -4)"
        FAIL=$((FAIL + 1))
    fi
}

# check_clean TEST_NAME KDIAG_ARGS...
#   --check must exit 0 and report no diagnostics.
check_clean() {
    local test_name="$1"
    shift

    local output status
    output=$("$KDIAG" --process --check "$@" 2>&1)
    status=$?

    if [ $status -eq 0 ] && echo "$output" | grep -q "0 diagnostics"; then
        echo "${green}PASS${reset} $test_name"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $test_name — expected a clean check"
        echo "       output: $(echo "$output" | head -4)"
        FAIL=$((FAIL + 1))
    fi
}

# check_count TEST_NAME N KDIAG_ARGS...
#   --check must report exactly N diagnostics.
check_count() {
    local test_name="$1"
    local expected="$2"
    shift 2

    local output got
    output=$("$KDIAG" --process --check "$@" 2>&1)
    got=$(echo "$output" | sed -nE 's/^([0-9]+) diagnostics?,.*/\1/p')

    if [ "$got" = "$expected" ]; then
        echo "${green}PASS${reset} $test_name"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $test_name — expected $expected diagnostics, got ${got:-none}"
        echo "       output: $(echo "$output" | head -4)"
        FAIL=$((FAIL + 1))
    fi
}

GREET='@@greet name : Hello, *name*. @@'

echo "${bold}Static klammer checking tests${reset}"
echo "============================="
echo

# --- What the renderer cannot reach ---

check_finds " 1. undefined klammer in an unselected @cond branch (in a body)" \
    "@nosuch is not defined" \
    "@@pick p : @cond *p* | @nosuch x @ | ok @ @@ @pick false @"

check_finds " 2. wrong arity in an unselected @cond branch (in a body)" \
    "is given 3" \
    "$GREET @@pick p : @cond *p* | @greet a | b | c @ | ok @ @@ @pick false @"

# A top-level @cond branch is now checked like any other.  It was the one
# documented gap in the checker: a @cond written at the top level of a DOCUMENT
# was resolved when the file was READ, so its unselected branch was gone before
# anything could look at it.  Resolving @cond at APPLICATION time instead
# (notes/Klammertext_improvements.md §4.2, decided 2026-08-15) closed it -- the
# branches survive read, so both are visible to a pass that applies nothing.
check_finds " 2a. a top-level @cond branch is checked too" \
    "@nosuch is not defined" \
    "@cond false | @nosuch x @ | ok @"
check_finds " 2b. ... including the branch that WOULD be selected" \
    "@nosuch is not defined" \
    "@cond true | @nosuch x @ | ok @"

check_finds " 3. undefined klammer in a body that is never applied" \
    "@nosuch is not defined" \
    "@@unused : @nosuch x @ @@ nothing applies it"

check_finds " 4. the body it was found in is named" \
    "in body of @unused" \
    "@@unused : @nosuch x @ @@ nothing applies it"

# --- Arity ---

check_finds " 5. too few positional arguments" \
    "needs 2 positional arguments but is given 1" \
    '@@pair a | b : *a**b* @@ @pair x @'

check_finds " 6. too many positional arguments" \
    "takes 1 positional argument but is given 2" \
    "$GREET @greet a | b @"

check_finds " 7. undefined optional argument" \
    'has no optional argument ":nope"' \
    "$GREET @greet a :nope 1 @"

check_finds " 8. the accepted optional arguments are listed" \
    "It accepts: :loud" \
    '@@greet name :loud : *name* @@ @greet a :nope 1 @'

check_clean " 9. a rest argument accepts extra positional arguments" \
    '@@many a | rest.rest : *a* @@ @many x | y | z @'

# --- Nesting.  A bar or an option name belonging to a nested klammer is not
#     this klammer's; the checker counts at depth 0, as @cond does. ---

check_clean "10. nested klammer's bars are not counted as the outer's" \
    '@@frac a | b : *a*/*b* @@ @@one x : [*x*] @@ @one @frac 1 | 2 @ @'

check_clean "11. nested klammer's option name is not counted as the outer's" \
    '@@inner a :flag : *a* @@ @@outer x : [*x*] @@ @outer @inner q :flag y @ @'

# --- Target coverage ---

check_finds "12. klammer not defined for a target" \
    'is not defined for the target "tex"' \
    '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@only.html : H @@ @only@'

check_clean "13. defined for every target is clean" \
    '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@both : B @@ @both@'

# --- What the checker deliberately does not see ---

# In a body, so the @eval is not evaluated at read time: what is being tested
# is that the checker does not read the eval's ARGUMENT as an application.
check_clean "14. @eval argument content is code, not applications" \
    '@@w : @eval len("@nosuch") @ @@'

check_clean "15. a literal parameter's content is raw text" \
    '@@lit t.literal : *t* @@ @lit @nosuch x @ lit@'

# --- Reporting ---

check_count "16. a target-independent fault is reported once, not per target" \
    1 \
    '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@g : @nosuch@ @@'

check_clean "17. a correct document checks clean" \
    "$GREET @greet World @"

# An absolute path: "make -C tst test" runs from tst/, and @read resolves
# against the current directory.
check_clean "18. the Standard Klammer Set checks clean" \
    "@read $K/sks/sks.k @"

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