;; FFI tests — define-foreign, *foreign-registry*, foreign-dispatch

(defsuite
  "foreign-registry-basic"
  (deftest
    "define-foreign registers in *foreign-registry*"
    (define-foreign my-abs (x :as number) :returns :number :js "Math.abs")
    (assert (foreign-registered? "my-abs")))
  (deftest
    "foreign-lookup returns spec dict"
    (define-foreign my-floor (x :as number) :returns :number :js "Math.floor")
    (let
      ((spec (foreign-lookup "my-floor")))
      (assert= (get spec "name") "my-floor")
      (assert= (get spec "js") "Math.floor")
      (assert= (get spec "returns") "number")))
  (deftest
    "foreign-names includes registered names"
    (define-foreign my-ceil (x :as number) :returns :number :js "Math.ceil")
    (assert (contains? (foreign-names) "my-ceil")))
  (deftest
    "define-foreign creates callable lambda"
    (define-foreign my-round (x :as number) :returns :number :js "Math.round")
    (assert (lambda? my-round)))
  (deftest
    "multiple define-foreign coexist"
    (define-foreign ff-a () :js "Date.now")
    (define-foreign ff-b (s :as string) :js "parseInt")
    (assert (foreign-registered? "ff-a"))
    (assert (foreign-registered? "ff-b"))))

(defsuite
  "foreign-param-parsing"
  (deftest
    "single param with type"
    (define-foreign fp-one (url :as string) :js "encodeURI")
    (let
      ((spec (foreign-lookup "fp-one")))
      (let
        ((params (get spec "params")))
        (assert= (len params) 1)
        (assert= (get (first params) "name") "url")
        (assert= (get (first params) "type") "string"))))
  (deftest
    "multiple params with types"
    (define-foreign fp-two (base :as string radix :as number) :js "parseInt")
    (let
      ((spec (foreign-lookup "fp-two")))
      (let
        ((params (get spec "params")))
        (assert= (len params) 2)
        (assert= (get (first params) "name") "base")
        (assert= (get (first params) "type") "string")
        (assert= (get (nth params 1) "name") "radix")
        (assert= (get (nth params 1) "type") "number"))))
  (deftest
    "no params"
    (define-foreign fp-none () :js "Date.now")
    (let
      ((spec (foreign-lookup "fp-none")))
      (assert= (len (get spec "params")) 0)))
  (deftest
    "param without :as defaults to any"
    (define-foreign fp-any (x) :js "String")
    (let
      ((spec (foreign-lookup "fp-any")))
      (let
        ((params (get spec "params")))
        (assert= (get (first params) "type") "any"))))
  (deftest
    "callback param type"
    (define-foreign fp-cb (handler :as callback) :js "setTimeout")
    (let
      ((spec (foreign-lookup "fp-cb")))
      (assert= (get (first (get spec "params")) "type") "callback"))))

(defsuite
  "foreign-binding-resolution"
  (deftest
    "dotted binding splits into object + method"
    (let
      ((resolved (foreign-resolve-binding "localStorage.getItem")))
      (assert= (get resolved "object") "localStorage")
      (assert= (get resolved "method") "getItem")))
  (deftest
    "simple binding has nil object"
    (let
      ((resolved (foreign-resolve-binding "parseInt")))
      (assert= (get resolved "object") nil)
      (assert= (get resolved "method") "parseInt")))
  (deftest
    "deep dotted binding preserves object path"
    (let
      ((resolved (foreign-resolve-binding "window.navigator.language")))
      (assert= (get resolved "object") "window.navigator")
      (assert= (get resolved "method") "language")))
  (deftest
    "single segment is method only"
    (let
      ((resolved (foreign-resolve-binding "alert")))
      (assert= (get resolved "object") nil)
      (assert= (get resolved "method") "alert"))))

(defsuite
  "foreign-kwargs"
  (deftest
    "returns keyword parsed correctly"
    (define-foreign fk-ret (x :as number) :returns :number :js "Math.abs")
    (assert= (get (foreign-lookup "fk-ret") "returns") "number"))
  (deftest
    "doc keyword stored"
    (define-foreign fk-doc () :js "Date.now" :doc "Get current timestamp")
    (assert= (get (foreign-lookup "fk-doc") "doc") "Get current timestamp"))
  (deftest
    "capability keyword stored"
    (define-foreign
      fk-cap
      (url :as string)
      :returns :promise
      :js "window.fetch"
      :capability :network)
    (assert= (get (foreign-lookup "fk-cap") "capability") "network"))
  (deftest
    "promise return type"
    (define-foreign
      fk-async
      (url :as string)
      :returns :promise
      :js "window.fetch")
    (assert= (get (foreign-lookup "fk-async") "returns") "promise")))

(defsuite
  "foreign-build-lambda"
  (deftest
    "sync foreign builds non-perform lambda"
    (define-foreign fbl-sync (x :as number) :returns :number :js "Math.abs")
    (let
      ((spec (foreign-lookup "fbl-sync")))
      (let
        ((expr (foreign-build-lambda spec)))
        (assert (list? expr))
        (assert= (symbol-name (first expr)) "fn"))))
  (deftest
    "async foreign builds perform-wrapping lambda"
    (define-foreign
      fbl-async
      (url :as string)
      :returns :promise
      :js "window.fetch")
    (let
      ((spec (foreign-lookup "fbl-async")))
      (let
        ((expr (foreign-build-lambda spec)))
        (assert (list? expr))
        (let
          ((body (nth expr 2)))
          (assert= (symbol-name (first body)) "perform"))))))

(defsuite
  "foreign-type-checking"
  (deftest
    "foreign-check-args accepts correct types"
    (foreign-check-args "test" (list {:type "number" :name "x"}) (list 42))
    (assert true))
  (deftest
    "foreign-check-args rejects wrong type"
    (let
      ((err (guard (e (true (error-message e))) (foreign-check-args "test" (list {:type "number" :name "x"}) (list "not-a-number")) nil)))
      (assert (contains? err "expected number"))))
  (deftest
    "foreign-check-args accepts any type"
    (foreign-check-args "test" (list {:type "any" :name "x"}) (list "hello"))
    (foreign-check-args "test" (list {:type "any" :name "x"}) (list 42))
    (assert true))
  (deftest
    "foreign-check-args rejects too few args"
    (let
      ((err (guard (e (true (error-message e))) (foreign-check-args "test" (list {:type "number" :name "x"} {:type "number" :name "y"}) (list 1)) nil)))
      (assert (contains? err "expected 2 args")))))