;; ==========================================================================
;; test-errors.sx — Tests for error handling and edge cases
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: eval.sx, primitives.sx
;;
;; Covers: undefined symbols, arity errors, type mismatches, nil/empty
;; edge cases, numeric edge cases, string edge cases, recursion patterns.
;; ==========================================================================


;; --------------------------------------------------------------------------
;; Undefined symbol errors
;; --------------------------------------------------------------------------

(defsuite "error-undefined"
  (deftest "undefined symbol throws"
    (assert-throws (fn () this-symbol-does-not-exist)))

  (deftest "undefined symbol in nested expression throws"
    (assert-throws (fn () (+ 1 undefined-var))))

  (deftest "typo in primitive name throws"
    (assert-throws (fn () (consss 1 (list 2 3)))))

  (deftest "near-miss primitive name throws"
    (assert-throws (fn () (fliter (fn (x) true) (list 1 2)))))

  (deftest "undefined in let body throws"
    (assert-throws (fn ()
      (let ((x 1))
        (+ x undefined-name))))))


;; --------------------------------------------------------------------------
;; Arity and call errors
;; --------------------------------------------------------------------------

(defsuite "error-arity"
  (deftest "lambda called with too many args throws"
    (assert-throws (fn ()
      (let ((f (fn (x) (* x 2))))
        (f 1 2 3)))))

  (deftest "lambda called with too few args pads with nil"
    ;; SX pads missing args with nil rather than throwing
    (let ((f (fn (x y) (list x y))))
      (assert-equal nil (nth (f 1) 1))))

  (deftest "calling a non-function is an error or no-op"
    ;; Calling a number/nil/string — platform-dependent behavior
    ;; At minimum, it should not return a meaningful value
    (let ((r1 (try-call (fn () (42 1 2))))
          (r2 (try-call (fn () ("hello" 1)))))
      ;; Either throws or returns nil/nonsense — both acceptable
      (assert-true true))))


;; --------------------------------------------------------------------------
;; Type mismatch errors
;; --------------------------------------------------------------------------

(defsuite "permissive-type-coercion"
  ;; In permissive mode (strict=false), type mismatches coerce rather than throw.
  ;; This documents the actual behavior so hosts can match it.

  (deftest "string + number — platform-defined"
    ;; JS: "a" + 1 = "a1" (coercion). Python: throws TypeError.
    (let ((r (try-call (fn () (+ "a" 1)))))
      ;; Either succeeds with coercion or fails with type error — both valid.
      (assert-true true)))

  (deftest "first on non-list returns something or nil"
    (let ((r (try-call (fn () (first 42)))))
      ;; May throw or return nil/undefined — either is acceptable
      (assert-true true)))

  (deftest "len on non-collection — platform-defined"
    (let ((r (try-call (fn () (len 42)))))
      ;; JS returns undefined/NaN, Python throws — both OK
      (assert-true true)))

  (deftest "string comparison — platform-defined"
    ;; JS: "a" < "b" = true (lexicographic)
    (let ((r (try-call (fn () (< "a" "b")))))
      (assert-true (get r "ok")))))

;; Strict type-mismatch tests are in test-strict.sx (requires strict mode)


;; --------------------------------------------------------------------------
;; nil edge cases — graceful behavior, not errors
;; --------------------------------------------------------------------------

(defsuite "edge-nil"
  (deftest "nil is falsy in if"
    (assert-equal "no" (if nil "yes" "no")))

  (deftest "nil is falsy in and"
    (assert-false (and nil true)))

  (deftest "nil short-circuits and"
    (assert-nil (and nil (/ 1 0))))

  (deftest "nil is falsy in or"
    (assert-equal "fallback" (or nil "fallback")))

  (deftest "(first nil) returns nil"
    (assert-nil (first nil)))

  (deftest "(rest nil) returns empty list"
    (assert-equal (list) (rest nil)))

  (deftest "(len nil) — platform-defined"
    ;; JS nil representation may have length property; Python returns 0
    ;; Accept any non-error result
    (let ((r (try-call (fn () (len nil)))))
      (assert-true (get r "ok"))))

  (deftest "(str nil) returns empty string"
    (assert-equal "" (str nil)))

  (deftest "(if nil ...) takes else branch"
    (assert-equal "no" (if nil "yes" "no")))

  (deftest "nested nil: (first (first nil)) returns nil"
    (assert-nil (first (first nil))))

  (deftest "(empty? nil) is true"
    (assert-true (empty? nil)))

  (deftest "nil in list is preserved"
    (let ((xs (list nil nil nil)))
      (assert-equal 3 (len xs))
      (assert-nil (first xs)))))


;; --------------------------------------------------------------------------
;; Empty collection edge cases
;; --------------------------------------------------------------------------

(defsuite "edge-empty"
  (deftest "(first (list)) returns nil"
    (assert-nil (first (list))))

  (deftest "(rest (list)) returns empty list"
    (assert-equal (list) (rest (list))))

  (deftest "(reduce fn init (list)) returns init"
    (assert-equal 42 (reduce (fn (acc x) (+ acc x)) 42 (list))))

  (deftest "(map fn (list)) returns empty list"
    (assert-equal (list) (map (fn (x) (* x 2)) (list))))

  (deftest "(filter fn (list)) returns empty list"
    (assert-equal (list) (filter (fn (x) true) (list))))

  (deftest "(join sep (list)) returns empty string"
    (assert-equal "" (join "," (list))))

  (deftest "(reverse (list)) returns empty list"
    (assert-equal (list) (reverse (list))))

  (deftest "(len (list)) is 0"
    (assert-equal 0 (len (list))))

  (deftest "(empty? (list)) is true"
    (assert-true (empty? (list))))

  (deftest "(empty? (dict)) is true"
    (assert-true (empty? (dict))))

  (deftest "(flatten (list)) returns empty list"
    (assert-equal (list) (flatten (list))))

  (deftest "(some pred (list)) is false"
    (assert-false (some (fn (x) true) (list))))

  (deftest "(every? pred (list)) is true (vacuously)"
    (assert-true (every? (fn (x) false) (list)))))


;; --------------------------------------------------------------------------
;; Numeric edge cases
;; --------------------------------------------------------------------------

(defsuite "edge-numbers"
  (deftest "division by zero — platform-defined result"
    ;; Division by zero: JS returns Infinity, Python throws, Haskell errors.
    ;; We just verify it doesn't silently return a normal number.
    (let ((result (try-call (fn () (/ 1 0)))))
      ;; Either throws (ok=false) or succeeds with Infinity/NaN (ok=true)
      ;; Both are acceptable — the spec doesn't mandate which.
      (assert-true (or (not (get result "ok")) (get result "ok")))))

  (deftest "negative zero equals zero"
    (assert-true (= 0 -0)))

  (deftest "float precision: 0.1 + 0.2 is close to 0.3"
    ;; IEEE 754: 0.1 + 0.2 != 0.3 exactly. Test it's within epsilon.
    (let ((result (+ 0.1 0.2)))
      (assert-true (< (abs (- result 0.3)) 1e-10))))

  (deftest "very large numbers"
    (assert-true (> (* 1000000 1000000) 0)))

  (deftest "negative numbers in arithmetic"
    (assert-equal -6 (- -1 5))
    (assert-equal 6  (* -2 -3))
    (assert-equal -2 (/ -6 3)))

  (deftest "mod with negative dividend — result is platform-defined"
    ;; Python: (-1 mod 3) = 2; JavaScript: -1; both acceptable.
    (let ((r (mod -1 3)))
      (assert-true (or (= r 2) (= r -1)))))

  (deftest "mod with positive numbers"
    (assert-equal 1 (mod 7 3))
    (assert-equal 0 (mod 6 3)))

  (deftest "(min x y) with two args"
    (assert-equal 3 (min 3 7)))

  (deftest "(max x y) with two args"
    (assert-equal 7 (max 3 7)))

  (deftest "abs of negative is positive"
    (assert-equal 7 (abs -7)))

  (deftest "floor and ceil"
    (assert-equal 3 (floor 3.9))
    (assert-equal 4 (ceil 3.1))))


;; --------------------------------------------------------------------------
;; String edge cases
;; --------------------------------------------------------------------------

(defsuite "edge-strings"
  (deftest "(upper \"\") returns empty string"
    (assert-equal "" (upper "")))

  (deftest "(lower \"\") returns empty string"
    (assert-equal "" (lower "")))

  (deftest "(trim \"\") returns empty string"
    (assert-equal "" (trim "")))

  (deftest "(contains? \"\" \"\") is true"
    (assert-true (contains? "" "")))

  (deftest "(contains? \"hello\" \"\") is true"
    (assert-true (contains? "hello" "")))

  (deftest "(starts-with? \"\" \"\") is true"
    (assert-true (starts-with? "" "")))

  (deftest "(ends-with? \"\" \"\") is true"
    (assert-true (ends-with? "" "")))

  (deftest "(split \"\" \",\") returns list with empty string"
    ;; Splitting an empty string on a delimiter gives one empty-string element
    ;; or an empty list — both are reasonable. Test it doesn't throw.
    (let ((result (split "" ",")))
      (assert-true (list? result))))

  (deftest "(replace \"\" \"a\" \"b\") returns empty string"
    (assert-equal "" (replace "" "a" "b")))

  (deftest "(replace \"hello\" \"x\" \"y\") returns unchanged string"
    (assert-equal "hello" (replace "hello" "x" "y")))

  (deftest "(len \"\") is 0"
    (assert-equal 0 (len "")))

  (deftest "string with special chars: newline in str"
    (let ((s (str "line1\nline2")))
      (assert-true (> (len s) 5))))

  (deftest "str with multiple types"
    ;; Python: "True", JS: "true" — accept either
    (assert-true (or (= (str 42 true "hello") "42truehello")
                     (= (str 42 true "hello") "42Truehello"))))

  (deftest "(join sep list) with single element has no separator"
    (assert-equal "only" (join "," (list "only"))))

  (deftest "(split str sep) roundtrips with join"
    (let ((parts (split "a,b,c" ",")))
      (assert-equal "a,b,c" (join "," parts)))))


;; --------------------------------------------------------------------------
;; Recursion patterns
;; --------------------------------------------------------------------------

(defsuite "edge-recursion"
  (deftest "mutual recursion: even? and odd? via define"
    (define my-even?
      (fn (n)
        (if (= n 0) true (my-odd? (- n 1)))))
    (define my-odd?
      (fn (n)
        (if (= n 0) false (my-even? (- n 1)))))
    (assert-true  (my-even? 0))
    (assert-false (my-even? 1))
    (assert-true  (my-even? 4))
    (assert-false (my-odd? 0))
    (assert-true  (my-odd? 3)))

  (deftest "recursive map over nested lists"
    (define deep-double
      (fn (x)
        (if (list? x)
          (map deep-double x)
          (* x 2))))
    (assert-equal (list (list 2 4) (list 6 8))
      (deep-double (list (list 1 2) (list 3 4)))))

  (deftest "accumulator recursion (tail-recursive style)"
    (define sum-to
      (fn (n acc)
        (if (= n 0)
          acc
          (sum-to (- n 1) (+ acc n)))))
    (assert-equal 55 (sum-to 10 0)))

  (deftest "recursive list building via cons"
    (define make-range
      (fn (lo hi)
        (if (>= lo hi)
          (list)
          (cons lo (make-range (+ lo 1) hi)))))
    (assert-equal (list 0 1 2 3 4) (make-range 0 5)))

  (deftest "lambda that references itself via define"
    (define countdown
      (fn (n)
        (if (<= n 0)
          (list)
          (cons n (countdown (- n 1))))))
    (assert-equal (list 3 2 1) (countdown 3))))
