;; ==========================================================================
;; test-forms.sx — Tests for web-platform definition forms
;;
;; Requires: test-framework.sx, forms.sx loaded.
;; Tests defpage, streaming functions, and the multi-stream data protocol.
;;
;; These tests were previously in spec/tests/test-eval.sx but belong here
;; because they test web-specific forms, not the core evaluator.
;; ==========================================================================


;; --------------------------------------------------------------------------
;; defpage — page definition form
;; --------------------------------------------------------------------------

(defsuite "defpage"
  (deftest "basic defpage returns page-def"
    (let ((p (defpage test-basic :path "/test" :auth :public :content (div "hello"))))
      (assert-true (not (nil? p)))
      (assert-equal "test-basic" (get p "name"))
      (assert-equal "/test" (get p "path"))
      (assert-equal "public" (get p "auth"))))

  (deftest "defpage content expr is unevaluated AST"
    (let ((p (defpage test-content :path "/c" :auth :public :content (~my-comp :title "hi"))))
      (assert-true (not (nil? (get p "content"))))))

  (deftest "defpage with :stream"
    (let ((p (defpage test-stream :path "/s" :auth :public :stream true :content (div "x"))))
      (assert-equal true (get p "stream"))))

  (deftest "defpage with :shell"
    (let ((p (defpage test-shell :path "/sh" :auth :public :stream true
               :shell (~my-layout (~suspense :id "data" :fallback (div "loading...")))
               :content (~my-streamed :data data-val))))
      (assert-true (not (nil? (get p "shell"))))
      (assert-true (not (nil? (get p "content"))))))

  (deftest "defpage with :fallback"
    (let ((p (defpage test-fallback :path "/f" :auth :public :stream true
               :fallback (div :class "skeleton" "loading")
               :content (div "done"))))
      (assert-true (not (nil? (get p "fallback"))))))

  (deftest "defpage with :data"
    (let ((p (defpage test-data :path "/d" :auth :public
               :data (fetch-items)
               :content (~items-list :items items))))
      (assert-true (not (nil? (get p "data"))))))

  (deftest "defpage missing fields are nil"
    (let ((p (defpage test-minimal :path "/m" :auth :public :content (div "x"))))
      (assert-nil (get p "data"))
      (assert-nil (get p "filter"))
      (assert-nil (get p "aside"))
      (assert-nil (get p "menu"))
      (assert-nil (get p "shell"))
      (assert-nil (get p "fallback"))
      (assert-equal false (get p "stream")))))


;; --------------------------------------------------------------------------
;; Multi-stream data protocol
;; --------------------------------------------------------------------------

(defsuite "stream-chunk-id"
  (deftest "extracts stream-id from chunk"
    (assert-equal "my-slot" (stream-chunk-id {"stream-id" "my-slot" "x" 1})))

  (deftest "defaults to stream-content when missing"
    (assert-equal "stream-content" (stream-chunk-id {"x" 1 "y" 2}))))

(defsuite "stream-chunk-bindings"
  (deftest "removes stream-id from chunk"
    (let ((bindings (stream-chunk-bindings {"stream-id" "slot" "name" "alice" "age" 30})))
      (assert-equal "alice" (get bindings "name"))
      (assert-equal 30 (get bindings "age"))
      (assert-nil (get bindings "stream-id"))))

  (deftest "returns all keys when no stream-id"
    (let ((bindings (stream-chunk-bindings {"a" 1 "b" 2})))
      (assert-equal 1 (get bindings "a"))
      (assert-equal 2 (get bindings "b")))))

(defsuite "normalize-binding-key"
  (deftest "converts underscores to hyphens"
    (assert-equal "my-key" (normalize-binding-key "my_key")))

  (deftest "leaves hyphens unchanged"
    (assert-equal "my-key" (normalize-binding-key "my-key")))

  (deftest "handles multiple underscores"
    (assert-equal "a-b-c" (normalize-binding-key "a_b_c"))))

(defsuite "bind-stream-chunk"
  (deftest "creates fresh env with bindings"
    (let ((base {"existing" 42})
          (chunk {"stream-id" "slot" "user-name" "bob" "count" 5})
          (env (bind-stream-chunk chunk base)))
      ;; Base env bindings are preserved
      (assert-equal 42 (get env "existing"))
      ;; Chunk bindings are added (stream-id removed)
      (assert-equal "bob" (get env "user-name"))
      (assert-equal 5 (get env "count"))
      ;; stream-id is not in env
      (assert-nil (get env "stream-id"))))

  (deftest "isolates env from base — bindings don't leak to base"
    (let ((base {"x" 1})
          (chunk {"stream-id" "s" "y" 2})
          (env (bind-stream-chunk chunk base)))
      ;; Chunk bindings should not appear in base
      (assert-nil (get base "y"))
      ;; Base bindings should be in derived env
      (assert-equal 1 (get env "x")))))

(defsuite "validate-stream-data"
  (deftest "valid: list of dicts"
    (assert-true (validate-stream-data
      (list {"stream-id" "a" "x" 1}
            {"stream-id" "b" "y" 2}))))

  (deftest "valid: empty list"
    (assert-true (validate-stream-data (list))))

  (deftest "invalid: single dict (not a list)"
    (assert-equal false (validate-stream-data {"x" 1})))

  (deftest "invalid: list containing non-dict"
    (assert-equal false (validate-stream-data (list {"x" 1} "oops" {"y" 2})))))


;; --------------------------------------------------------------------------
;; Multi-stream end-to-end scenarios
;; --------------------------------------------------------------------------

(defsuite "multi-stream routing"
  (deftest "stream-chunk-id routes different chunks to different slots"
    (let ((chunks (list
            {"stream-id" "stream-fast" "msg" "quick"}
            {"stream-id" "stream-medium" "msg" "steady"}
            {"stream-id" "stream-slow" "msg" "slow"}))
          (ids (map stream-chunk-id chunks)))
      (assert-equal "stream-fast" (nth ids 0))
      (assert-equal "stream-medium" (nth ids 1))
      (assert-equal "stream-slow" (nth ids 2))))

  (deftest "bind-stream-chunk creates isolated envs per chunk"
    (let ((base {"layout" "main"})
          (chunk-a {"stream-id" "a" "title" "First" "count" 1})
          (chunk-b {"stream-id" "b" "title" "Second" "count" 2})
          (env-a (bind-stream-chunk chunk-a base))
          (env-b (bind-stream-chunk chunk-b base)))
      ;; Each env has its own bindings
      (assert-equal "First" (get env-a "title"))
      (assert-equal "Second" (get env-b "title"))
      (assert-equal 1 (get env-a "count"))
      (assert-equal 2 (get env-b "count"))
      ;; Both share base
      (assert-equal "main" (get env-a "layout"))
      (assert-equal "main" (get env-b "layout"))
      ;; Neither leaks into base
      (assert-nil (get base "title"))))

  (deftest "normalize-binding-key applied to chunk keys"
    (let ((chunk {"stream-id" "s" "user_name" "alice" "item_count" 3})
          (bindings (stream-chunk-bindings chunk)))
      ;; Keys with underscores need normalizing for SX env
      (assert-equal "alice" (get bindings "user_name"))
      ;; normalize-binding-key converts them
      (assert-equal "user-name" (normalize-binding-key "user_name"))
      (assert-equal "item-count" (normalize-binding-key "item_count"))))

  (deftest "defpage stream flag defaults to false"
    (let ((p (defpage test-no-stream :path "/ns" :auth :public :content (div "x"))))
      (assert-equal false (get p "stream"))))

  (deftest "defpage stream true recorded in page-def"
    (let ((p (defpage test-with-stream :path "/ws" :auth :public
               :stream true
               :shell (~layout (~suspense :id "data"))
               :content (~chunk :val val))))
      (assert-equal true (get p "stream"))
      (assert-true (not (nil? (get p "shell")))))))
