; Tcl-on-SX eval tests
(define tcl-eval-pass 0)
(define tcl-eval-fail 0)
(define tcl-eval-failures (list))

(define
  tcl-eval-assert
  (fn
    (label expected actual)
    (if
      (equal? expected actual)
      (set! tcl-eval-pass (+ tcl-eval-pass 1))
      (begin
        (set! tcl-eval-fail (+ tcl-eval-fail 1))
        (append!
          tcl-eval-failures
          (str label ": expected=" (str expected) " got=" (str actual)))))))

(define
  tcl-run-eval-tests
  (fn
    ()
    (set! tcl-eval-pass 0)
    (set! tcl-eval-fail 0)
    (set! tcl-eval-failures (list))
    (define interp (fn () (make-default-tcl-interp)))
    (define run (fn (src) (tcl-eval-string (interp) src)))
    (define
      ok
      (fn (label actual expected) (tcl-eval-assert label expected actual)))
    (define
      ok?
      (fn (label condition) (tcl-eval-assert label true condition)))
    (tcl-eval-assert "set-result" "hello" (get (run "set x hello") :result))
    (tcl-eval-assert
      "set-stored"
      "hello"
      (tcl-var-get (run "set x hello") "x"))
    (tcl-eval-assert
      "var-sub"
      "hello"
      (tcl-var-get (run "set x hello\nset y $x") "y"))
    (tcl-eval-assert
      "puts"
      "world\n"
      (get (run "set x world\nputs $x") :output))
    (tcl-eval-assert
      "puts-nonewline"
      "hi"
      (get (run "puts -nonewline hi") :output))
    (tcl-eval-assert "incr" "6" (tcl-var-get (run "set x 5\nincr x") "x"))
    (tcl-eval-assert
      "incr-delta"
      "8"
      (tcl-var-get (run "set x 5\nincr x 3") "x"))
    (tcl-eval-assert
      "incr-neg"
      "7"
      (tcl-var-get (run "set x 10\nincr x -3") "x"))
    (tcl-eval-assert
      "append"
      "foobar"
      (tcl-var-get (run "set x foo\nappend x bar") "x"))
    (tcl-eval-assert
      "append-new"
      "hello"
      (tcl-var-get (run "append x hello") "x"))
    (tcl-eval-assert
      "cmdsub-result"
      "6"
      (get (run "set x 5\nset y [incr x]") :result))
    (tcl-eval-assert
      "cmdsub-y"
      "6"
      (tcl-var-get (run "set x 5\nset y [incr x]") "y"))
    (tcl-eval-assert
      "cmdsub-x"
      "6"
      (tcl-var-get (run "set x 5\nset y [incr x]") "x"))
    (tcl-eval-assert
      "multi-cmd"
      "second"
      (get (run "set x first\nset x second") :result))
    (tcl-eval-assert "semi-x" "1" (tcl-var-get (run "set x 1; set y 2") "x"))
    (tcl-eval-assert "semi-y" "2" (tcl-var-get (run "set x 1; set y 2") "y"))
    (tcl-eval-assert
      "braced-nosub"
      "$x"
      (tcl-var-get (run "set x 42\nset y {$x}") "y"))
    (tcl-eval-assert
      "concat-word"
      "foobar"
      (tcl-var-get (run "set x foo\nset y ${x}bar") "y"))
    (tcl-eval-assert
      "set-get"
      "world"
      (get (run "set x world\nset x") :result))
    (tcl-eval-assert
      "puts-channel"
      "hello\n"
      (get (run "puts stdout hello") :output))
    (ok "if-true" (get (run "set x 0\nif {1} {set x 1}") :result) "1")
    (ok "if-false" (get (run "set x 0\nif {0} {set x 1}") :result) "0")
    (ok
      "if-else-t"
      (tcl-var-get (run "if {1} {set x yes} else {set x no}") "x")
      "yes")
    (ok
      "if-else-f"
      (tcl-var-get (run "if {0} {set x yes} else {set x no}") "x")
      "no")
    (ok
      "if-cmp"
      (tcl-var-get
        (run "set x 5\nif {$x > 3} {set r big} else {set r small}")
        "r")
      "big")
    (ok
      "while"
      (tcl-var-get
        (run "set i 0\nset s 0\nwhile {$i < 5} {incr i\nincr s $i}")
        "s")
      "15")
    (ok
      "while-break"
      (tcl-var-get
        (run "set i 0\nwhile {1} {incr i\nif {$i == 3} {break}}")
        "i")
      "3")
    (ok
      "for"
      (tcl-var-get
        (run "set s 0\nfor {set i 1} {$i <= 5} {incr i} {incr s $i}")
        "s")
      "15")
    (ok
      "foreach"
      (tcl-var-get (run "set s 0\nforeach x {1 2 3 4 5} {incr s $x}") "s")
      "15")
    (ok
      "foreach-list"
      (get (run "set acc \"\"\nforeach w {hello world} {append acc $w}") :result)
      "helloworld")
    (ok
      "lappend"
      (tcl-var-get (run "lappend lst a\nlappend lst b\nlappend lst c") "lst")
      "a b c")
    (ok?
      "unset-gone"
      (let
        ((i (run "set x 42\nunset x")))
        (let
          ((frame (get i :frame)))
          (nil? (get (get frame :locals) "x")))))
    (ok "eval" (tcl-var-get (run "eval {set x hello}") "x") "hello")
    (ok "expr-precedence" (get (run "expr {3 + 4 * 2}") :result) "11")
    (ok "expr-parens" (get (run "expr {(3 + 4) * 2}") :result) "14")
    (ok "expr-unary-minus" (get (run "expr {-5}") :result) "-5")
    (ok "expr-unary-not-0" (get (run "expr {!0}") :result) "1")
    (ok "expr-unary-not-1" (get (run "expr {!1}") :result) "0")
    (ok "expr-power" (get (run "expr {2 ** 10}") :result) "1024")
    (ok "expr-le" (get (run "expr {3 <= 3}") :result) "1")
    (ok "expr-ge" (get (run "expr {4 >= 5}") :result) "0")
    (ok "expr-and" (get (run "expr {1 && 1}") :result) "1")
    (ok "expr-or" (get (run "expr {0 || 1}") :result) "1")
    (ok "expr-var-sub" (get (run "set x 7\nexpr {$x * 3}") :result) "21")
    (ok "expr-abs-neg" (get (run "expr {abs(-3)}") :result) "3")
    (ok "expr-abs-pos" (get (run "expr {abs(5)}") :result) "5")
    (ok "expr-pow-fn" (get (run "expr {pow(2, 8)}") :result) "256")
    (ok "expr-max" (get (run "expr {max(3, 7)}") :result) "7")
    (ok "expr-min" (get (run "expr {min(3, 7)}") :result) "3")
    (ok "expr-sqrt-9" (get (run "expr {sqrt(9)}") :result) "3")
    (ok "expr-sqrt-16" (get (run "expr {sqrt(16)}") :result) "4")
    (ok "expr-mod" (get (run "expr {17 % 5}") :result) "2")
    (ok "expr-nospace" (get (run "expr {3+4*2}") :result) "11")
    (ok "expr-add" (get (run "expr {3 + 4}") :result) "7")
    (ok "expr-cmp" (get (run "expr {5 > 3}") :result) "1")
    (ok
      "break-stops"
      (tcl-var-get (run "set x 0\nwhile {1} {set x 1\nbreak\nset x 99}") "x")
      "1")
    (ok
      "continue"
      (tcl-var-get
        (run
          "set s 0\nfor {set i 1} {$i <= 5} {incr i} {if {$i == 3} {continue}\nincr s $i}")
        "s")
      "12")
    (ok
      "switch"
      (tcl-var-get
        (run "set x foo\nswitch $x {{foo} {set r yes} {bar} {set r no}}")
        "r")
      "yes")
    (ok
      "switch-default"
      (tcl-var-get
        (run "set x baz\nswitch $x {{foo} {set r yes} default {set r other}}")
        "r")
      "other")
    (ok
      "nested-if"
      (tcl-var-get
        (run
          "set x 5\nif {$x > 10} {set r big} elseif {$x > 3} {set r mid} else {set r small}")
        "r")
      "mid")
    (ok "str-length" (get (run "string length hello") :result) "5")
    (ok "str-length-empty" (get (run "string length {}") :result) "0")
    (ok "str-index" (get (run "string index hello 1") :result) "e")
    (ok "str-index-oob" (get (run "string index hello 99") :result) "")
    (ok "str-range" (get (run "string range hello 1 3") :result) "ell")
    (ok "str-range-clamp" (get (run "string range hello 3 99") :result) "lo")
    (ok "str-compare-eq" (get (run "string compare abc abc") :result) "0")
    (ok "str-compare-lt" (get (run "string compare abc abd") :result) "-1")
    (ok "str-compare-gt" (get (run "string compare b a") :result) "1")
    (ok "str-match-star" (get (run "string match h*o hello") :result) "1")
    (ok "str-match-q" (get (run "string match h?llo hello") :result) "1")
    (ok "str-match-no" (get (run "string match h*x hello") :result) "0")
    (ok "str-toupper" (get (run "string toupper hello") :result) "HELLO")
    (ok "str-tolower" (get (run "string tolower WORLD") :result) "world")
    (ok "str-trim" (get (run "string trim {  hi  }") :result) "hi")
    (ok "str-trimleft" (get (run "string trimleft {  hi  }") :result) "hi  ")
    (ok "str-trimright" (get (run "string trimright {  hi  }") :result) "  hi")
    (ok "str-trim-chars" (get (run "string trim {xxhelloxx} x") :result) "hello")
    (ok "str-map" (get (run "string map {a X b Y} {abc}") :result) "XYc")
    (ok "str-repeat" (get (run "string repeat ab 3") :result) "ababab")
    (ok "str-first" (get (run "string first ll hello") :result) "2")
    (ok "str-first-miss" (get (run "string first z hello") :result) "-1")
    (ok "str-last" (get (run "string last l hello") :result) "3")
    (ok "str-is-int" (get (run "string is integer 42") :result) "1")
    (ok "str-is-not-int" (get (run "string is integer foo") :result) "0")
    (ok "str-is-alpha" (get (run "string is alpha hello") :result) "1")
    (ok "str-is-alpha-no" (get (run "string is alpha hello1") :result) "0")
    (ok "str-is-boolean" (get (run "string is boolean true") :result) "1")
    (ok "str-cat" (get (run "string cat foo bar baz") :result) "foobarbaz")
    ; --- list command tests ---
    (ok "list-simple" (get (run "list a b c") :result) "a b c")
    (ok "list-brace-elem" (get (run "list {a b} c") :result) "{a b} c")
    (ok "list-empty" (get (run "list") :result) "")
    (ok "lindex-1" (get (run "lindex {a b c} 1") :result) "b")
    (ok "lindex-0" (get (run "lindex {a b c} 0") :result) "a")
    (ok "lindex-oob" (get (run "lindex {a b c} 5") :result) "")
    (ok "lrange" (get (run "lrange {a b c d} 1 2") :result) "b c")
    (ok "lrange-full" (get (run "lrange {a b c} 0 end") :result) "a b c")
    (ok "llength" (get (run "llength {a b c}") :result) "3")
    (ok "llength-empty" (get (run "llength {}") :result) "0")
    (ok "lreverse" (get (run "lreverse {1 2 3}") :result) "3 2 1")
    (ok "lsearch-found" (get (run "lsearch {a b c} b") :result) "1")
    (ok "lsearch-missing" (get (run "lsearch {a b c} z") :result) "-1")
    (ok "lsearch-exact" (get (run "lsearch -exact {foo bar} foo") :result) "0")
    (ok "lsort-asc" (get (run "lsort {banana apple cherry}") :result) "apple banana cherry")
    (ok "lsort-int" (get (run "lsort -integer {10 2 30 5}") :result) "2 5 10 30")
    (ok "lsort-dec" (get (run "lsort -decreasing {c a b}") :result) "c b a")
    (ok "lreplace" (get (run "lreplace {a b c d} 1 2 X Y") :result) "a X Y d")
    (ok "linsert" (get (run "linsert {a b c} 1 X Y") :result) "a X Y b c")
    (ok "linsert-end" (get (run "linsert {a b} end Z") :result) "a b Z")
    (ok "concat" (get (run "concat {a b} {c d}") :result) "a b c d")
    (ok "split-sep" (get (run "split {a:b:c} :") :result) "a b c")
    (ok "split-ws" (get (run "split {a b c}") :result) "a b c")
    (ok "join-sep" (get (run "join {a b c} -") :result) "a-b-c")
    (ok "join-default" (get (run "join {a b c}") :result) "a b c")
    (ok "list-var" (get (run "set L {x y z}\nllength $L") :result) "3")
    ; --- dict command tests ---
    (ok "dict-create" (get (run "dict create a 1 b 2") :result) "a 1 b 2")
    (ok "dict-create-empty" (get (run "dict create") :result) "")
    (ok "dict-get" (get (run "dict get {a 1 b 2} a") :result) "1")
    (ok "dict-get-b" (get (run "dict get {a 1 b 2} b") :result) "2")
    (ok "dict-exists-yes" (get (run "dict exists {a 1 b 2} a") :result) "1")
    (ok "dict-exists-no" (get (run "dict exists {a 1 b 2} z") :result) "0")
    (ok "dict-set-new" (get (run "set d {}\ndict set d x 42") :result) "x 42")
    (ok "dict-set-update" (get (run "set d {a 1 b 2}\ndict set d a 99") :result) "a 99 b 2")
    (ok "dict-set-stored" (tcl-var-get (run "set d {a 1}\ndict set d b 2") "d") "a 1 b 2")
    (ok "dict-unset" (get (run "set d {a 1 b 2}\ndict unset d a") :result) "b 2")
    (ok "dict-unset-stored" (tcl-var-get (run "set d {a 1 b 2}\ndict unset d a") "d") "b 2")
    (ok "dict-keys" (get (run "dict keys {a 1 b 2}") :result) "a b")
    (ok "dict-keys-pattern" (get (run "dict keys {abc 1 abd 2 xyz 3} ab*") :result) "abc abd")
    (ok "dict-values" (get (run "dict values {a 1 b 2}") :result) "1 2")
    (ok "dict-size" (get (run "dict size {a 1 b 2 c 3}") :result) "3")
    (ok "dict-size-empty" (get (run "dict size {}") :result) "0")
    (ok "dict-for" (tcl-var-get (run "set acc {}\ndict for {k v} {a 1 b 2} {append acc $k$v}") "acc") "a1b2")
    (ok "dict-merge-disjoint" (get (run "dict merge {a 1} {b 2}") :result) "a 1 b 2")
    (ok "dict-merge-overlap" (get (run "dict merge {a 1 b 2} {b 99}") :result) "a 1 b 99")
    (ok "dict-incr-existing" (get (run "set d {x 5}\ndict incr d x") :result) "x 6")
    (ok "dict-incr-delta" (get (run "set d {x 5}\ndict incr d x 3") :result) "x 8")
    (ok "dict-incr-missing" (get (run "set d {}\ndict incr d n") :result) "n 1")
    (ok "dict-append" (get (run "set d {x hello}\ndict append d x _hi") :result) "x hello_hi")
    (ok "dict-append-new" (get (run "set d {}\ndict append d k val") :result) "k val")
    ; --- proc tests ---
    (ok "proc-basic" (get (run "proc add {a b} {expr {$a + $b}}\nadd 3 4") :result) "7")
    (ok "proc-return" (get (run "proc greet {name} {set msg \"hi $name\"\nreturn $msg}\ngreet World") :result) "hi World")
    (ok "proc-factorial" (get (run "proc factorial {n} {if {$n <= 1} {return 1}\nexpr {$n * [factorial [expr {$n - 1}]]}}\nfactorial 5") :result) "120")
    (ok "proc-args" (get (run "proc sum args {set t 0\nforeach x $args {incr t $x}\nreturn $t}\nsum 1 2 3 4") :result) "10")
    (ok "proc-isolated" (get (run "set x outer\nproc p {} {set x inner\nreturn $x}\np") :result) "inner")
    (ok "proc-caller-unchanged" (tcl-var-get (run "set x outer\nproc p {} {set x inner\nreturn $x}\np\nset dummy 1") "x") "outer")
    (ok "proc-output" (get (run "proc hello {} {puts -nonewline hi}\nhello") :output) "hi")
    ; --- upvar tests ---
    (ok "upvar-incr" (tcl-var-get (run "proc incr2 {varname} {upvar 1 $varname v\nincr v}\nset counter 10\nincr2 counter\nset counter") "counter") "11")
    (ok "upvar-double" (tcl-var-get (run "proc double-it {varname} {upvar 1 $varname x\nset x [expr {$x * 2}]}\nset val 5\ndouble-it val\nset val") "val") "10")
    (ok "upvar-result" (get (run "proc double-it {varname} {upvar 1 $varname x\nset x [expr {$x * 2}]}\nset val 5\ndouble-it val\nset val") :result) "10")
    ; --- uplevel tests ---
    (ok "uplevel-set" (tcl-var-get (run "proc setvar {name val} {uplevel 1 \"set $name $val\"}\nsetvar x 99\nset x") "x") "99")
    (ok "uplevel-get" (get (run "proc getvar {name} {uplevel 1 \"set $name\"}\nset y 77\ngetvar y") :result) "77")
    ; --- global tests ---
    (ok "global-read" (get (run "set g 100\nproc getg {} {global g\nreturn $g}\ngetg") :result) "100")
    (ok "global-write" (tcl-var-get (run "set g 0\nproc bumping {} {global g\nincr g}\nbumping\nbumping\nset g") "g") "2")
    ; --- info tests ---
    (ok "info-level-0" (get (run "info level") :result) "0")
    (ok "info-level-proc" (get (run "proc p {} {info level}\np") :result) "1")
    (ok "info-procs" (let ((r (get (run "proc myfn {} {}\ninfo procs") :result))) (contains? (tcl-list-split r) "myfn")) true)
    (ok "info-args" (get (run "proc add {a b} {expr {$a+$b}}\ninfo args add") :result) "a b")
    (ok "info-commands-has-set" (let ((r (get (run "info commands") :result))) (contains? (tcl-list-split r) "set")) true)
    ; --- classic programs ---
    (ok
      "classic-for-each-line"
      (get
        (run "proc for-each-line {var lines body} {\n    foreach item $lines {\n        uplevel 1 [list set $var $item]\n        uplevel 1 $body\n    }\n}\nset total 0\nfor-each-line line {hello world foo} {\n    incr total [string length $line]\n}\nset total")
        :result)
      "13")
    (ok
      "classic-assert"
      (get
        (run "proc assert {expr_str} {\n    set result [uplevel 1 [list expr $expr_str]]\n    if {!$result} {\n        error \"Assertion failed: $expr_str\"\n    }\n}\nset x 42\nassert {$x == 42}\nassert {$x > 0}\nset x 10\nassert {$x < 100}\nset x")
        :result)
      "10")
    (ok
      "classic-with-temp-var"
      (get
        (run "proc with-temp-var {varname tempval body} {\n    upvar 1 $varname v\n    set saved $v\n    set v $tempval\n    uplevel 1 $body\n    set v $saved\n}\nset x 100\nwith-temp-var x 999 {\n    set captured $x\n}\nlist $x $captured")
        :result)
      "100 999")
    (ok
      "array-set-get"
      (get
        (run "array set a {x 1 y 2 z 3}; array get a x")
        :result)
      "x 1")
    (ok
      "array-names"
      (get
        (run "array set a {p 10 q 20}; lsort [array names a]")
        :result)
      "p q")
    (ok
      "array-size"
      (get
        (run "array set a {x 1 y 2 z 3}; array size a")
        :result)
      "3")
    (ok
      "array-exists-true"
      (get
        (run "array set a {x 1}; array exists a")
        :result)
      "1")
    (ok
      "array-exists-false"
      (get
        (run "array exists nosucharray")
        :result)
      "0")
    (ok
      "array-unset-key"
      (get
        (run "array set a {x 1 y 2 z 3}; array unset a y; lsort [array names a]")
        :result)
      "x z")
    (ok
      "array-scalar-access"
      (get
        (run "set a(foo) hello; set a(bar) world; set a(foo)")
        :result)
      "hello")
    (ok
      "array-get-all"
      (get
        (run "set a(k) v; set pairs [array get a]; llength $pairs")
        :result)
      "2")
    (dict
      "passed"
      tcl-eval-pass
      "failed"
      tcl-eval-fail
      "failures"
      tcl-eval-failures)))
