;; ==========================================================================
;; test-eval.sx — Tests for the core evaluator and primitives
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: eval.sx, primitives.sx
;; ==========================================================================


;; --------------------------------------------------------------------------
;; Literals and types
;; --------------------------------------------------------------------------

(defsuite
  "literals"
  (deftest
    "numbers are numbers"
    (assert-type "number" 42)
    (assert-type "number" 3.14)
    (assert-type "number" -1))
  (deftest
    "strings are strings"
    (assert-type "string" "hello")
    (assert-type "string" ""))
  (deftest
    "booleans are booleans"
    (assert-type "boolean" true)
    (assert-type "boolean" false))
  (deftest "nil is nil" (assert-type "nil" nil) (assert-nil nil))
  (deftest
    "lists are lists"
    (assert-type "list" (list 1 2 3))
    (assert-type "list" (list)))
  (deftest "dicts are dicts" (assert-type "dict" {:b 2 :a 1})))


;; --------------------------------------------------------------------------
;; Arithmetic
;; --------------------------------------------------------------------------

(defsuite
  "arithmetic"
  (deftest
    "addition"
    (assert-equal 3 (+ 1 2))
    (assert-equal 0 (+ 0 0))
    (assert-equal -1 (+ 1 -2))
    (assert-equal 10 (+ 1 2 3 4)))
  (deftest
    "subtraction"
    (assert-equal 1 (- 3 2))
    (assert-equal -1 (- 2 3)))
  (deftest
    "multiplication"
    (assert-equal 6 (* 2 3))
    (assert-equal 0 (* 0 100))
    (assert-equal 24 (* 1 2 3 4)))
  (deftest
    "division"
    (assert-equal 2 (/ 6 3))
    (assert-equal 2.5 (/ 5 2)))
  (deftest
    "modulo"
    (assert-equal 1 (mod 7 3))
    (assert-equal 0 (mod 6 3))))


;; --------------------------------------------------------------------------
;; Comparison
;; --------------------------------------------------------------------------

(defsuite
  "comparison"
  (deftest
    "equality"
    (assert-true (= 1 1))
    (assert-false (= 1 2))
    (assert-true (= "a" "a"))
    (assert-false (= "a" "b")))
  (deftest
    "deep equality"
    (assert-true
      (equal?
        (list 1 2 3)
        (list 1 2 3)))
    (assert-false
      (equal? (list 1 2) (list 1 3)))
    (assert-true (equal? {:a 1} {:a 1}))
    (assert-false (equal? {:a 1} {:a 2})))
  (deftest
    "ordering"
    (assert-true (< 1 2))
    (assert-false (< 2 1))
    (assert-true (> 2 1))
    (assert-true (<= 1 1))
    (assert-true (<= 1 2))
    (assert-true (>= 2 2))
    (assert-true (>= 3 2))))


;; --------------------------------------------------------------------------
;; String operations
;; --------------------------------------------------------------------------

(defsuite
  "strings"
  (deftest
    "str concatenation"
    (assert-equal "abc" (str "a" "b" "c"))
    (assert-equal "hello world" (str "hello" " " "world"))
    (assert-equal "42" (str 42))
    (assert-equal "" (str)))
  (deftest
    "string-length"
    (assert-equal 5 (string-length "hello"))
    (assert-equal 0 (string-length "")))
  (deftest
    "substring"
    (assert-equal "ell" (substring "hello" 1 4))
    (assert-equal "hello" (substring "hello" 0 5)))
  (deftest
    "string-contains?"
    (assert-true (string-contains? "hello world" "world"))
    (assert-false (string-contains? "hello" "xyz")))
  (deftest
    "upcase and downcase"
    (assert-equal "HELLO" (upcase "hello"))
    (assert-equal "hello" (downcase "HELLO")))
  (deftest
    "trim"
    (assert-equal "hello" (trim "  hello  "))
    (assert-equal "hello" (trim "hello")))
  (deftest
    "split and join"
    (assert-equal (list "a" "b" "c") (split "a,b,c" ","))
    (assert-equal "a-b-c" (join "-" (list "a" "b" "c")))))


;; --------------------------------------------------------------------------
;; List operations
;; --------------------------------------------------------------------------

(defsuite
  "lists"
  (deftest
    "constructors"
    (assert-equal
      (list 1 2 3)
      (list 1 2 3))
    (assert-equal (list) (list))
    (assert-length 3 (list 1 2 3)))
  (deftest
    "first and rest"
    (assert-equal 1 (first (list 1 2 3)))
    (assert-equal
      (list 2 3)
      (rest (list 1 2 3)))
    (assert-nil (first (list)))
    (assert-equal (list) (rest (list))))
  (deftest
    "nth"
    (assert-equal
      1
      (nth (list 1 2 3) 0))
    (assert-equal
      2
      (nth (list 1 2 3) 1))
    (assert-equal
      3
      (nth (list 1 2 3) 2)))
  (deftest
    "last"
    (assert-equal 3 (last (list 1 2 3)))
    (assert-nil (last (list))))
  (deftest
    "cons and append"
    (assert-equal
      (list 0 1 2)
      (cons 0 (list 1 2)))
    (assert-equal
      (list 1 2 3 4)
      (append (list 1 2) (list 3 4))))
  (deftest
    "reverse"
    (assert-equal
      (list 3 2 1)
      (reverse (list 1 2 3)))
    (assert-equal (list) (reverse (list))))
  (deftest
    "empty?"
    (assert-true (empty? (list)))
    (assert-false (empty? (list 1))))
  (deftest
    "len"
    (assert-equal 0 (len (list)))
    (assert-equal 3 (len (list 1 2 3))))
  (deftest
    "contains?"
    (assert-true
      (contains? (list 1 2 3) 2))
    (assert-false
      (contains? (list 1 2 3) 4)))
  (deftest
    "flatten"
    (assert-equal
      (list 1 2 3 4)
      (flatten
        (list (list 1 2) (list 3 4))))))


;; --------------------------------------------------------------------------
;; Dict operations
;; --------------------------------------------------------------------------

(defsuite
  "dicts"
  (deftest
    "dict literal"
    (assert-type "dict" {:b 2 :a 1})
    (assert-equal 1 (get {:a 1} "a"))
    (assert-equal 2 (get {:b 2 :a 1} "b")))
  (deftest
    "assoc"
    (assert-equal {:b 2 :a 1} (assoc {:a 1} "b" 2))
    (assert-equal {:a 99} (assoc {:a 1} "a" 99)))
  (deftest "dissoc" (assert-equal {:b 2} (dissoc {:b 2 :a 1} "a")))
  (deftest
    "keys and vals"
    (let
      ((d {:b 2 :a 1}))
      (assert-length 2 (keys d))
      (assert-length 2 (vals d))
      (assert-contains "a" (keys d))
      (assert-contains "b" (keys d))))
  (deftest
    "has-key?"
    (assert-true (has-key? {:a 1} "a"))
    (assert-false (has-key? {:a 1} "b")))
  (deftest
    "merge"
    (assert-equal {:c 3 :b 2 :a 1} (merge {:b 2 :a 1} {:c 3}))
    (assert-equal {:b 2 :a 99} (merge {:b 2 :a 1} {:a 99}))))


;; --------------------------------------------------------------------------
;; Predicates
;; --------------------------------------------------------------------------

(defsuite
  "predicates"
  (deftest
    "nil?"
    (assert-true (nil? nil))
    (assert-false (nil? 0))
    (assert-false (nil? false))
    (assert-false (nil? "")))
  (deftest
    "number?"
    (assert-true (number? 42))
    (assert-true (number? 3.14))
    (assert-false (number? "42")))
  (deftest
    "string?"
    (assert-true (string? "hello"))
    (assert-false (string? 42)))
  (deftest
    "list?"
    (assert-true (list? (list 1 2)))
    (assert-false (list? "not a list")))
  (deftest
    "dict?"
    (assert-true (dict? {:a 1}))
    (assert-false (dict? (list 1))))
  (deftest
    "boolean?"
    (assert-true (boolean? true))
    (assert-true (boolean? false))
    (assert-false (boolean? nil))
    (assert-false (boolean? 0)))
  (deftest
    "not"
    (assert-true (not false))
    (assert-true (not nil))
    (assert-false (not true))
    (assert-false (not 1))
    (assert-false (not "x"))))


;; --------------------------------------------------------------------------
;; Special forms
;; --------------------------------------------------------------------------

(defsuite
  "special-forms"
  (deftest
    "if"
    (assert-equal "yes" (if true "yes" "no"))
    (assert-equal "no" (if false "yes" "no"))
    (assert-equal "no" (if nil "yes" "no"))
    (assert-nil (if false "yes")))
  (deftest
    "when"
    (assert-equal "yes" (when true "yes"))
    (assert-nil (when false "yes")))
  (deftest
    "cond"
    (assert-equal "a" (cond true "a" :else "b"))
    (assert-equal "b" (cond false "a" :else "b"))
    (assert-equal "c" (cond false "a" false "b" :else "c")))
  (deftest
    "cond with 2-element predicate as first test"
    (assert-equal 0 (cond (nil? nil) 0 :else 1))
    (assert-equal 1 (cond (nil? "x") 0 :else 1))
    (assert-equal "empty" (cond (empty? (list)) "empty" :else "not-empty"))
    (assert-equal
      "not-empty"
      (cond (empty? (list 1)) "empty" :else "not-empty"))
    (assert-equal "yes" (cond (not false) "yes" :else "no"))
    (assert-equal "no" (cond (not true) "yes" :else "no")))
  (deftest
    "cond with 2-element predicate and no :else"
    (assert-equal "found" (cond (nil? nil) "found" (nil? "x") "other"))
    (assert-equal "b" (cond (nil? "x") "a" (not false) "b")))
  (deftest
    "and"
    (assert-true (and true true))
    (assert-false (and true false))
    (assert-false (and false true))
    (assert-equal 3 (and 1 2 3)))
  (deftest
    "or"
    (assert-equal 1 (or 1 2))
    (assert-equal 2 (or false 2))
    (assert-equal "fallback" (or nil false "fallback"))
    (assert-false (or false false)))
  (deftest
    "let"
    (assert-equal
      3
      (let ((x 1) (y 2)) (+ x y)))
    (assert-equal
      "hello world"
      (let ((a "hello") (b " world")) (str a b))))
  (deftest
    "let clojure-style"
    (assert-equal 3 (let (x 1 y 2) (+ x y))))
  (deftest
    "do / begin"
    (assert-equal 3 (do 1 2 3))
    (assert-equal "last" (begin "first" "middle" "last")))
  (deftest "define" (define x 42) (assert-equal 42 x))
  (deftest
    "set!"
    (define x 1)
    (set! x 2)
    (assert-equal 2 x)))


;; --------------------------------------------------------------------------
;; Lambda and closures
;; --------------------------------------------------------------------------

(defsuite
  "lambdas"
  (deftest
    "basic lambda"
    (let
      ((add (fn (a b) (+ a b))))
      (assert-equal 3 (add 1 2))))
  (deftest
    "closure captures env"
    (let
      ((x 10))
      (let
        ((add-x (fn (y) (+ x y))))
        (assert-equal 15 (add-x 5)))))
  (deftest
    "lambda as argument"
    (assert-equal
      (list 2 4 6)
      (map
        (fn (x) (* x 2))
        (list 1 2 3))))
  (deftest
    "recursive lambda via define"
    (define
      factorial
      (fn
        (n)
        (if
          (<= n 1)
          1
          (* n (factorial (- n 1))))))
    (assert-equal 120 (factorial 5)))
  (deftest
    "higher-order returns lambda"
    (let
      ((make-adder (fn (n) (fn (x) (+ n x)))))
      (let
        ((add5 (make-adder 5)))
        (assert-equal 8 (add5 3)))))
  (deftest
    "multi-body lambda returns last value"
    (let
      ((f (fn (x) (+ x 1) (+ x 2) (+ x 3))))
      (assert-equal 13 (f 10))))
  (deftest
    "multi-body lambda side effects via dict mutation"
    (let
      ((state (dict "a" 0 "b" 0)))
      (let
        ((f (fn () (dict-set! state "a" 1) (dict-set! state "b" 2) "done")))
        (assert-equal "done" (f))
        (assert-equal 1 (get state "a"))
        (assert-equal 2 (get state "b")))))
  (deftest
    "multi-body lambda two expressions"
    (assert-equal
      20
      ((fn (x) (+ x 1) (* x 2)) 10))
    (assert-equal 42 ((fn () (+ 1 2) 42)))))


;; --------------------------------------------------------------------------
;; Higher-order forms
;; --------------------------------------------------------------------------

(defsuite
  "higher-order"
  (deftest
    "map"
    (assert-equal
      (list 2 4 6)
      (map
        (fn (x) (* x 2))
        (list 1 2 3)))
    (assert-equal (list) (map (fn (x) x) (list))))
  (deftest
    "filter"
    (assert-equal
      (list 2 4)
      (filter
        (fn (x) (= (mod x 2) 0))
        (list 1 2 3 4)))
    (assert-equal
      (list)
      (filter (fn (x) false) (list 1 2 3))))
  (deftest
    "reduce"
    (assert-equal
      10
      (reduce
        (fn (acc x) (+ acc x))
        0
        (list 1 2 3 4)))
    (assert-equal
      0
      (reduce (fn (acc x) (+ acc x)) 0 (list))))
  (deftest
    "some"
    (assert-true
      (some
        (fn (x) (> x 3))
        (list 1 2 3 4 5)))
    (assert-false
      (some
        (fn (x) (> x 10))
        (list 1 2 3))))
  (deftest
    "every?"
    (assert-true
      (every?
        (fn (x) (> x 0))
        (list 1 2 3)))
    (assert-false
      (every?
        (fn (x) (> x 2))
        (list 1 2 3))))
  (deftest
    "map-indexed"
    (assert-equal
      (list "0:a" "1:b" "2:c")
      (map-indexed (fn (i x) (str i ":" x)) (list "a" "b" "c")))))


;; --------------------------------------------------------------------------
;; Components
;; --------------------------------------------------------------------------

(defsuite
  "components"
  (deftest
    "defcomp creates component"
    (defcomp ~test-comp (&key title) (div title))
    (assert-true (not (nil? ~test-comp))))
  (deftest
    "component renders with keyword args"
    (defcomp ~greeting (&key name) (span (str "Hello, " name "!")))
    (assert-true (not (nil? ~greeting))))
  (deftest
    "component with children"
    (defcomp ~box (&key &rest children) (div :class "box" children))
    (assert-true (not (nil? ~box))))
  (deftest
    "component with default via or"
    (defcomp ~label (&key text) (span (or text "default")))
    (assert-true (not (nil? ~label))))
  (deftest
    "defcomp default affinity is auto"
    (defcomp ~aff-default (&key x) (div x))
    (assert-equal "auto" (component-affinity ~aff-default)))
  (deftest
    "defcomp affinity client"
    (defcomp ~aff-client (&key x) :affinity :client (div x))
    (assert-equal "client" (component-affinity ~aff-client)))
  (deftest
    "defcomp affinity server"
    (defcomp ~aff-server (&key x) :affinity :server (div x))
    (assert-equal "server" (component-affinity ~aff-server)))
  (deftest
    "defcomp affinity preserves body"
    (defcomp ~aff-body (&key val) :affinity :client (span val))
    (assert-equal "client" (component-affinity ~aff-body))
    (assert-true (not (nil? ~aff-body)))))


;; --------------------------------------------------------------------------
;; Macros
;; --------------------------------------------------------------------------

(defsuite
  "macros"
  (deftest
    "defmacro creates macro"
    (defmacro
      unless
      (cond &rest body)
      (quasiquote (if (not (unquote cond)) (do (splice-unquote body)))))
    (assert-equal "yes" (unless false "yes"))
    (assert-nil (unless true "no")))
  (deftest
    "quasiquote and unquote"
    (let
      ((x 42))
      (assert-equal
        (list 1 42 3)
        (quasiquote (1 (unquote x) 3)))))
  (deftest
    "splice-unquote"
    (let
      ((xs (list 2 3 4)))
      (assert-equal
        (list 1 2 3 4 5)
        (quasiquote (1 (splice-unquote xs) 5))))))


;; --------------------------------------------------------------------------
;; Threading macro
;; --------------------------------------------------------------------------

(defsuite
  "threading"
  (deftest
    "thread-first"
    (assert-equal 8 (-> 5 (+ 1) (+ 2)))
    (assert-equal "HELLO" (-> "hello" upcase))
    (assert-equal "HELLO WORLD" (-> "hello" (str " world") upcase))))


;; --------------------------------------------------------------------------
;; Truthiness
;; --------------------------------------------------------------------------

(defsuite
  "truthiness"
  (deftest
    "truthy values"
    (assert-true (if 1 true false))
    (assert-true (if "x" true false))
    (assert-true (if (list 1) true false))
    (assert-true (if true true false)))
  (deftest
    "falsy values"
    (assert-false (if false true false))
    (assert-false (if nil true false))))


;; --------------------------------------------------------------------------
;; Edge cases and regression tests
;; --------------------------------------------------------------------------

(defsuite
  "edge-cases"
  (deftest
    "nested let scoping"
    (let
      ((x 1))
      (let ((x 2)) (assert-equal 2 x))))
  (deftest
    "recursive map"
    (assert-equal
      (list (list 2 4) (list 6 8))
      (map
        (fn (sub) (map (fn (x) (* x 2)) sub))
        (list (list 1 2) (list 3 4)))))
  (deftest
    "keyword as value"
    (assert-equal "class" :class)
    (assert-equal "id" :id))
  (deftest
    "dict with evaluated values"
    (let ((x 42)) (assert-equal 42 (get {:val x} "val"))))
  (deftest
    "nil propagation"
    (assert-nil (get {:a 1} "missing"))
    (assert-equal "default" (or (get {:a 1} "missing") "default")))
  (deftest
    "empty operations"
    (assert-equal (list) (map (fn (x) x) (list)))
    (assert-equal (list) (filter (fn (x) true) (list)))
    (assert-equal
      0
      (reduce (fn (acc x) (+ acc x)) 0 (list)))
    (assert-equal 0 (len (list)))
    (assert-equal "" (str))))
