#!/bin/bash
#
# option_set_test.sh — Regression tests for option sets (the ".o" target).
#
# An option set is a named group of OPTIONAL parameters, declared once and
# used by several klammers, so that a writer learns one vocabulary instead of
# a spelling per klammer:
#
#     @@caption_args.o :caption :number.bool true : A caption @@
#     @@table.k rows.rest(2) @caption_args@ : A table of rows of cells @@
#
# The rules this suite holds to:
#
#   * "o" is a pseudo-target beside "k".  A ".o" declaration defines no
#     klammer and produces no output for any target.
#   * A set declares optional parameters only.  A positional is not
#     writer-facing, so there is nothing for a set to standardize.
#   * Names and types come from the set; a DEFAULT may be overridden where
#     the set is used, because what varies between klammers is only what
#     silence means for that one klammer.
#   * A set may be used only in the parameter list of a ".k" declaration --
#     the one place a klammer's interface is declared once for all of its
#     targets.  Not in a per-target definition, and not in another set.
#   * A klammer application in a parameter list is an error.  Splicing a
#     constant klammer there used to be the way to share parameters, and in
#     a ".k" declaration it silently destroyed the whole parameter list.
#
# Engine tier: no SKS.  Every fixture defines its own target inline.
#
# Usage: ./option_set_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'

# The fixture prelude: a target, and two sets to use in declarations.
PRELUDE='@@@target fix | a fixture target @@@
@@cap.o :caption :number.bool true :side bottom : A caption for an element @@
@@pos.o :hpos left : Where an element sits @@'

# check_eq NAME EXPECTED SOURCE [TARGET] — render SOURCE (for the fix target
# unless TARGET says otherwise) and compare the trimmed output with EXPECTED.
check_eq() {
    local name="$1" expected="$2" source="$3" target="${4:-fix}"
    local output status
    output=$("$KTEXT" --klammersets none -s "$PRELUDE
$source" -t "$target" -d 2>&1)
    status=$?
    output=$(printf '%s' "$output" | tr -d '\n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
    if [ $status -ne 0 ]; then
        echo "${red}FAIL${reset} $name — ktext exited $status"
        echo "       output: $(printf '%s' "$output" | head -3)"
        FAIL=$((FAIL + 1))
        return
    fi
    if [ "$output" = "$expected" ]; then
        echo "${green}PASS${reset} $name"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $name"
        echo "       expected: [$expected]"
        echo "       got:      [$output]"
        FAIL=$((FAIL + 1))
    fi
}

# check_fails NAME SUBSTRING SOURCE — SOURCE must be rejected, with a
# message containing SUBSTRING.
check_fails() {
    local name="$1" needle="$2" source="$3"
    local output status
    output=$("$KTEXT" --klammersets none -s "$PRELUDE
$source" -t fix -d 2>&1)
    status=$?
    if [ $status -eq 0 ]; then
        echo "${red}FAIL${reset} $name — expected an error, ktext exited 0"
        FAIL=$((FAIL + 1))
        return
    fi
    if printf '%s' "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$needle"; then
        echo "${green}PASS${reset} $name"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $name"
        echo "       expected error containing: [$needle]"
        echo "       got: $(printf '%s' "$output" | tr '\n' ' ' | head -c 300)"
        FAIL=$((FAIL + 1))
    fi
}

# check_warns NAME SUBSTRING SOURCE — SOURCE must be accepted, and its
# combined output must contain SUBSTRING (used where a warning is expected
# beside the result, so an exact comparison would test the warning's wording).
# Since 2026-08-15 the override notice is reported at "-v 1" rather than
# warned about: ":::" exists to override, so the notice fired on the sanctioned
# use of a feature (see tst/deftype_test.sh's check_warn for the reasoning).
# The default run must be SILENT and the notice must appear at "-v 1".
check_warns() {
    local name="$1" needle="$2" source="$3"
    local output status quiet
    quiet=$("$KTEXT" --klammersets none -s "$PRELUDE
$source" -t fix -d 2>&1 >/dev/null)
    if [ -n "$quiet" ]; then
        echo "${red}FAIL${reset} $name — the default run was not silent"
        echo "       got: $(printf '%s' "$quiet" | head -1)"
        FAIL=$((FAIL + 1))
        return
    fi
    output=$("$KTEXT" --klammersets none -s "$PRELUDE
$source" -t fix -d -v 1 2>&1)
    status=$?
    if [ $status -ne 0 ]; then
        echo "${red}FAIL${reset} $name — ktext exited $status"
        FAIL=$((FAIL + 1))
        return
    fi
    if printf '%s' "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$needle"; then
        echo "${green}PASS${reset} $name"
        PASS=$((PASS + 1))
    else
        echo "${red}FAIL${reset} $name"
        echo "       expected a warning containing: [$needle]"
        echo "       got: $(printf '%s' "$output" | tr '\n' ' ' | head -c 300)"
        FAIL=$((FAIL + 1))
    fi
}

echo "${bold}Option set (.o) tests${reset}"
echo "====================="
echo

# --- The members become the klammer's parameters --------------------------

# A declaration using a set accepts the set's options, and the writer's
# arguments reach the body under the names the set declares.
check_eq " 1. a set's parameters are the klammer's" \
    "[A|top]" \
    '@@e.k x @cap@ : an element @@
     @@e.fix :: [*caption*|*side*] @@
     @e X :caption A :side top @'

# Absent argument, declared default: the set supplies it.
check_eq " 2. a member's default applies when the argument is absent" \
    "[|true|bottom]" \
    '@@e.k x @cap@ : an element @@
     @@e.fix :: [*caption*|*number*|*side*] @@
     @e X @'

# The use site may override a default -- and only the default.
check_eq " 3. a use-site override changes the default" \
    "[|true|top]" \
    '@@e.k x @cap :side top @ : an element @@
     @@e.fix :: [*caption*|*number*|*side*] @@
     @e X @'

check_eq " 4. a boolean default can be overridden to false" \
    "[false]" \
    '@@e.k x @cap :number false @ : an element @@
     @@e.fix :: [*number*] @@
     @e X @'

# An option written alone at the use site takes the argument type's :alone
# value, exactly as it does where an argument is written.
check_eq " 5. an override written alone takes the argtype's alone value" \
    "[true]" \
    '@@n.o :number.bool false : A number @@
     @@e.k x @n :number @ : an element @@
     @@e.fix :: [*number*] @@
     @e X @'

# Three levels: argument type, option set, use site -- and the writer's
# argument still wins over all of them.
check_eq " 6. a written argument wins over the overridden default" \
    "[maybe]" \
    '@@e.k x @cap :side top @ : an element @@
     @@e.fix :: [*side*] @@
     @e X :side maybe @'

check_eq " 7. two sets in one declaration" \
    "[bottom|left]" \
    '@@e.k x @cap@ @pos@ : an element @@
     @@e.fix :: [*side*|*hpos*] @@
     @e X @'

check_eq " 8. a set's parameters mix with the klammer's own" \
    "[own|bottom]" \
    '@@e.k x :mine own @cap@ : an element @@
     @@e.fix :: [*mine*|*side*] @@
     @e X @'

# Every target definition is an instance, so all of them get the expanded
# list: the set is resolved once, in the declaration.
check_eq " 9. a second target's instance inherits the same parameters" \
    "<bottom>" \
    '@@@target fix2 | another fixture target @@@
     @@e.k x @cap@ : an element @@
     @@e.fix :: [*side*] @@
     @@e.fix2 :: <*side*> @@
     @e X @' \
    fix2

# --- The declaration is not a klammer -------------------------------------

check_fails "10. a set defines no klammer" \
    'The klammer "cap" is not defined' \
    '@cap@'

# --- Where a set may be used ----------------------------------------------

check_fails "11. not in a per-target definition" \
    'may be used only in the parameter list of a ".k" declaration' \
    '@@e.fix x @cap@ : [*caption*] @@'

check_fails "12. not in a general definition" \
    'may be used only in the parameter list of a ".k" declaration' \
    '@@e x @cap@ : [*caption*] @@'

check_fails "13. not in another set" \
    'a set does not include another set' \
    '@@both.o :extra @cap@ : two vocabularies @@'

# --- A klammer application in a parameter list ----------------------------

check_fails "14. a klammer application in a parameter list is rejected" \
    'A klammer application in a parameter list is not allowed' \
    '@@c : :spliced @@
     @@e.k x @c@ : an element @@'

check_fails "15. ... and the message names the declared sets" \
    'Declared option sets: cap, pos' \
    '@@e.k x @nosuch@ : an element @@'

# --- Use-site overrides ---------------------------------------------------

check_fails "16. an override must name a member of the set" \
    'The option set "cap" has no parameter ":nope"' \
    '@@e.k x @cap :nope 1 @ : an element @@'

check_fails "17. an override value is validated at definition time" \
    'does not match the "bool" argument type' \
    '@@e.k x @cap :number perhaps @ : an element @@'

check_fails "18. an override may not restate the type" \
    'only a default may be given where it is used' \
    '@@e.k x @cap :number.bool false @ : an element @@'

check_fails "19. an override may not give a positional value" \
    'gives a value that is not an option' \
    '@@e.k x @cap here @ : an element @@'

# --- What a set may declare -----------------------------------------------

check_fails "20. a positional parameter is rejected" \
    'An option set declares only optional parameters' \
    '@@bad.o p :q : oops @@'

check_fails "21. a rest parameter is rejected" \
    'An option set declares only optional parameters' \
    '@@bad.o r.rest :q : oops @@'

check_fails "22. a set with no parameters is rejected" \
    'declares no parameters' \
    '@@bad.o : nothing at all @@'

check_fails '23. "::" has no meaning for a set' \
    'An option set is a declaration' \
    '@@cap.o :: nope @@'

# --- Collisions -----------------------------------------------------------

check_fails "24. two sets declaring the same name name both sets" \
    'from the option set "cap"' \
    '@@other.o :side right : another side @@
     @@e.k x @cap@ @other@ : an element @@'

check_fails "25. a set colliding with a declared parameter" \
    'declared in the parameter list' \
    '@@e.k x :side own @cap@ : an element @@'

# --- Redefinition ---------------------------------------------------------

check_fails "26. declaring a set twice is an error" \
    'Option set "cap.o" already defined' \
    '@@cap.o :caption : a second caption @@'

# A set is its own declaration, so an override restates what it declares --
# there is no ".k" for it to inherit a parameter list from.
check_warns '27. ":::" overrides a set, with a warning' \
    'overridden' \
    '@@cap.o :caption :number.bool true :side top ::: a replaced caption @@'

check_warns "28. ... and the overriding declaration is what a klammer gets" \
    "[top]" \
    '@@cap.o :caption :number.bool true :side top ::: a replaced caption @@
     @@e.k x @cap@ : an element @@
     @@e.fix :: [*side*] @@
     @e X @'

# A "::::" default is silently superseded by a later create, and the create
# is what the using declaration gets.
check_eq "29. a default set is superseded by a later declaration" \
    "[right]" \
    '@@d.o :where left :::: a default @@
     @@d.o :where right : the real one @@
     @@e.k x @d@ : an element @@
     @@e.fix :: [*where*] @@
     @e X @'

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