;; ==========================================================================
;; test-framework.sx — Reusable test macros and assertion helpers
;;
;; Loaded first by all test runners. Provides deftest, defsuite, and
;; assertion helpers. Requires 5 platform functions from the host:
;;
;;   try-call     (thunk)        -> {:ok true} | {:ok false :error "msg"}
;;   report-pass  (name)         -> platform-specific pass output
;;   report-fail  (name error)   -> platform-specific fail output
;;   push-suite   (name)         -> push suite name onto context stack
;;   pop-suite    ()             -> pop suite name from context stack
;;
;; Any host that provides these 5 functions can run any test spec.
;; ==========================================================================


;; --------------------------------------------------------------------------
;; 1. Test framework macros
;; --------------------------------------------------------------------------

(defmacro
  deftest
  (name &rest body)
  (quasiquote
    (when
      (test-allowed? (unquote name))
      (let
        ((result (try-call (fn () (splice-unquote body)))))
        (if
          (get result "ok")
          (report-pass (unquote name))
          (report-fail (unquote name) (get result "error")))))))

(defmacro defsuite (name &rest items)
  `(do (push-suite ,name)
       ,@items
       (pop-suite)))


;; --------------------------------------------------------------------------
;; 2. Assertion helpers — defined in SX, available in test bodies
;; --------------------------------------------------------------------------

(define assert-equal
  (fn (expected actual)
    (assert (equal? expected actual)
      (str "Expected " (str expected) " but got " (str actual)))))

(define assert-not-equal
  (fn (a b)
    (assert (not (equal? a b))
      (str "Expected values to differ but both are " (str a)))))

(define assert-true
  (fn (val)
    (assert val (str "Expected truthy but got " (str val)))))

(define assert-false
  (fn (val)
    (assert (not val) (str "Expected falsy but got " (str val)))))

(define assert-nil
  (fn (val)
    (assert (nil? val) (str "Expected nil but got " (str val)))))

(define assert-type
  (fn ((expected-type :as string) val)
    (let ((actual-type
            (if (nil? val) "nil"
              (if (boolean? val) "boolean"
                (if (number? val) "number"
                  (if (string? val) "string"
                    (if (list? val) "list"
                      (if (dict? val) "dict"
                        "unknown"))))))))
      (assert (= expected-type actual-type)
        (str "Expected type " expected-type " but got " actual-type)))))

(define assert-length
  (fn ((expected-len :as number) (col :as list))
    (assert (= (len col) expected-len)
      (str "Expected length " expected-len " but got " (len col)))))

(define assert-contains
  (fn (item (col :as list))
    (assert (some (fn (x) (equal? x item)) col)
      (str "Expected collection to contain " (str item)))))

(define assert-throws
  (fn ((thunk :as lambda))
    (let ((result (try-call thunk)))
      (assert (not (get result "ok"))
        "Expected an error to be thrown but none was"))))
