;; ==========================================================================
;; test-strict.sx — Tests for strict typing mode
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: eval.sx (strict-check-args, set-strict!, value-matches-type?)
;;
;; When *strict* is true, primitive calls check arg types at runtime.
;; ==========================================================================


;; --------------------------------------------------------------------------
;; value-matches-type? — the type predicate used by strict mode
;; --------------------------------------------------------------------------

(defsuite "value-matches-type"
  (deftest "number matches number"
    (assert-true (value-matches-type? 42 "number")))

  (deftest "string matches string"
    (assert-true (value-matches-type? "hello" "string")))

  (deftest "boolean matches boolean"
    (assert-true (value-matches-type? true "boolean")))

  (deftest "nil matches nil"
    (assert-true (value-matches-type? nil "nil")))

  (deftest "list matches list"
    (assert-true (value-matches-type? (list 1 2) "list")))

  (deftest "dict matches dict"
    (assert-true (value-matches-type? (dict "a" 1) "dict")))

  (deftest "any matches everything"
    (assert-true (value-matches-type? 42 "any"))
    (assert-true (value-matches-type? "s" "any"))
    (assert-true (value-matches-type? nil "any"))
    (assert-true (value-matches-type? (list) "any")))

  (deftest "wrong type fails"
    (assert-false (value-matches-type? "hello" "number"))
    (assert-false (value-matches-type? 42 "string"))
    (assert-false (value-matches-type? nil "number"))
    (assert-false (value-matches-type? true "number")))

  (deftest "nullable string accepts string or nil"
    (assert-true (value-matches-type? "hello" "string?"))
    (assert-true (value-matches-type? nil "string?"))
    (assert-false (value-matches-type? 42 "string?")))

  (deftest "nullable number accepts number or nil"
    (assert-true (value-matches-type? 42 "number?"))
    (assert-true (value-matches-type? nil "number?"))
    (assert-false (value-matches-type? "x" "number?"))))


;; --------------------------------------------------------------------------
;; Strict mode on/off
;; --------------------------------------------------------------------------

(defsuite "strict-mode-toggle"
  (deftest "strict is off by default"
    (assert-false *strict*))

  (deftest "set-strict! enables and disables"
    ;; Verify by testing behavior: with strict on, bad types throw
    (set-strict! true)
    (set-prim-param-types! {"inc" {"positional" (list (list "n" "number")) "rest-type" nil}})
    (let ((r (try-call (fn () (inc "x")))))
      (assert-false (get r "ok")))
    ;; Turn off: same call should succeed (JS coercion)
    (set-strict! false)
    (let ((r (try-call (fn () (inc "x")))))
      (assert-true (get r "ok")))
    (set-prim-param-types! nil)))


;; --------------------------------------------------------------------------
;; Strict mode catches type errors at runtime
;; --------------------------------------------------------------------------

(defsuite "strict-type-errors"
  ;; Enable strict mode and register param types for these tests
  (set-strict! true)
  (set-prim-param-types!
    {
      "+" {"positional" (list (list "a" "number")) "rest-type" "number"}
      "-" {"positional" (list (list "a" "number")) "rest-type" "number"}
      "*" {"positional" (list (list "a" "number")) "rest-type" "number"}
      "/" {"positional" (list (list "a" "number")) "rest-type" "number"}
      "inc" {"positional" (list (list "n" "number")) "rest-type" nil}
      "dec" {"positional" (list (list "n" "number")) "rest-type" nil}
      "upper" {"positional" (list (list "s" "string")) "rest-type" nil}
      "lower" {"positional" (list (list "s" "string")) "rest-type" nil}
      "first" {"positional" (list (list "coll" "list")) "rest-type" nil}
      "rest" {"positional" (list (list "coll" "list")) "rest-type" nil}
      "len" {"positional" (list (list "coll" "any")) "rest-type" nil}
      "keys" {"positional" (list (list "d" "dict")) "rest-type" nil}
    })

  (deftest "correct types pass"
    ;; These should NOT throw
    (assert-equal 3 (+ 1 2))
    (assert-equal "HELLO" (upper "hello"))
    (assert-equal 1 (first (list 1 2 3))))

  (deftest "string + number throws"
    (assert-throws (fn () (+ "a" 1))))

  (deftest "number + string throws"
    (assert-throws (fn () (+ 1 "b"))))

  (deftest "subtract string throws"
    (assert-throws (fn () (- "hello" 1))))

  (deftest "multiply string throws"
    (assert-throws (fn () (* 2 "three"))))

  (deftest "inc on string throws"
    (assert-throws (fn () (inc "x"))))

  (deftest "upper on number throws"
    (assert-throws (fn () (upper 42))))

  (deftest "first on number throws"
    (assert-throws (fn () (first 42))))

  (deftest "rest on number throws"
    (assert-throws (fn () (rest 42))))

  (deftest "keys on list throws"
    (assert-throws (fn () (keys (list 1 2 3)))))

  (deftest "nil is not a number"
    (assert-throws (fn () (+ nil 1))))

  (deftest "boolean is not a number"
    (assert-throws (fn () (* true 2))))

  (deftest "correct types after errors still pass"
    ;; Verify strict mode wasn't broken by previous throws
    (assert-equal 10 (+ 5 5))
    (assert-equal "HI" (upper "hi")))

  ;; Clean up — disable strict mode for other tests
  (set-strict! false)
  (set-prim-param-types! nil))
