;; ==========================================================================
;; test-deps.sx — Tests for component dependency analysis (deps.sx)
;;
;; Requires: test-framework.sx loaded first.
;; Platform functions: scan-refs, transitive-deps, components-needed,
;;   component-pure?, scan-io-refs, transitive-io-refs,
;;   scan-components-from-source, test-env
;;   (loaded from bootstrapped output by test runners)
;; ==========================================================================


;; --------------------------------------------------------------------------
;; Test component definitions — these exist in the test env for dep analysis
;; --------------------------------------------------------------------------

(defcomp ~dep-leaf ()
  (span "leaf"))

(defcomp ~dep-branch ()
  (div (~dep-leaf)))

(defcomp ~dep-trunk ()
  (div (~dep-branch) (~dep-leaf)))

(defcomp ~dep-conditional (&key show?)
  (if show?
    (~dep-leaf)
    (~dep-branch)))

(defcomp ~dep-nested-cond (&key mode)
  (cond
    (= mode "a") (~dep-leaf)
    (= mode "b") (~dep-branch)
    :else (~dep-trunk)))

(defcomp ~dep-island ()
  (div "no deps"))

;; Islands with dependencies — defisland bodies must be scanned
(defisland ~dep-island-with-child ()
  (div (~dep-leaf) "island content"))

(defisland ~dep-island-with-chain ()
  (div (~dep-branch) "deep island"))


;; --------------------------------------------------------------------------
;; 1. scan-refs — finds component references in AST nodes
;; --------------------------------------------------------------------------

(defsuite "scan-refs"

  (deftest "empty for string literal"
    (assert-equal (list) (scan-refs "hello")))

  (deftest "empty for number"
    (assert-equal (list) (scan-refs 42)))

  (deftest "finds component symbol"
    (let ((refs (scan-refs (quote (~dep-leaf)))))
      (assert-contains "~dep-leaf" refs)))

  (deftest "finds in nested list"
    (let ((refs (scan-refs (quote (div (span (~dep-leaf)))))))
      (assert-contains "~dep-leaf" refs)))

  (deftest "finds multiple refs"
    (let ((refs (scan-refs (quote (div (~dep-leaf) (~dep-branch))))))
      (assert-contains "~dep-leaf" refs)
      (assert-contains "~dep-branch" refs)))

  (deftest "deduplicates"
    (let ((refs (scan-refs (quote (div (~dep-leaf) (~dep-leaf))))))
      (assert-equal 1 (len refs))))

  (deftest "walks if branches"
    (let ((refs (scan-refs (quote (if true (~dep-leaf) (~dep-branch))))))
      (assert-contains "~dep-leaf" refs)
      (assert-contains "~dep-branch" refs)))

  (deftest "walks cond branches"
    (let ((refs (scan-refs (quote (cond (= x 1) (~dep-leaf) :else (~dep-trunk))))))
      (assert-contains "~dep-leaf" refs)
      (assert-contains "~dep-trunk" refs)))

  (deftest "ignores non-component symbols"
    (let ((refs (scan-refs (quote (div class "foo")))))
      (assert-equal 0 (len refs)))))


;; --------------------------------------------------------------------------
;; 2. scan-components-from-source — regex-based source string scanning
;; --------------------------------------------------------------------------

(defsuite "scan-components-from-source"

  (deftest "finds single component"
    (let ((refs (scan-components-from-source "(~dep-leaf)")))
      (assert-contains "~dep-leaf" refs)))

  (deftest "finds multiple components"
    (let ((refs (scan-components-from-source "(div (~dep-leaf) (~dep-branch))")))
      (assert-contains "~dep-leaf" refs)
      (assert-contains "~dep-branch" refs)))

  (deftest "no false positives on plain text"
    (let ((refs (scan-components-from-source "(div \"hello world\")")))
      (assert-equal 0 (len refs))))

  (deftest "handles hyphenated names"
    (let ((refs (scan-components-from-source "(~my-component :key val)")))
      (assert-contains "~my-component" refs))))


;; --------------------------------------------------------------------------
;; 3. transitive-deps — transitive dependency closure
;; --------------------------------------------------------------------------

(defsuite "transitive-deps"

  (deftest "leaf has no deps"
    (let ((deps (transitive-deps "~dep-leaf" (test-env))))
      (assert-equal 0 (len deps))))

  (deftest "direct dependency"
    (let ((deps (transitive-deps "~dep-branch" (test-env))))
      (assert-contains "~dep-leaf" deps)))

  (deftest "transitive closure"
    (let ((deps (transitive-deps "~dep-trunk" (test-env))))
      (assert-contains "~dep-branch" deps)
      (assert-contains "~dep-leaf" deps)))

  (deftest "excludes self"
    (let ((deps (transitive-deps "~dep-trunk" (test-env))))
      (assert-false (contains? deps "~dep-trunk"))))

  (deftest "walks conditional branches"
    (let ((deps (transitive-deps "~dep-conditional" (test-env))))
      (assert-contains "~dep-leaf" deps)
      (assert-contains "~dep-branch" deps)))

  (deftest "walks all cond branches"
    (let ((deps (transitive-deps "~dep-nested-cond" (test-env))))
      (assert-contains "~dep-leaf" deps)
      (assert-contains "~dep-branch" deps)
      (assert-contains "~dep-trunk" deps)))

  (deftest "island has no deps"
    (let ((deps (transitive-deps "~dep-island" (test-env))))
      (assert-equal 0 (len deps))))

  (deftest "accepts name without tilde"
    (let ((deps (transitive-deps "dep-branch" (test-env))))
      (assert-contains "~dep-leaf" deps)))

  (deftest "island direct dep scanned"
    (let ((deps (transitive-deps "~dep-island-with-child" (test-env))))
      (assert-contains "~dep-leaf" deps)))

  (deftest "island transitive deps scanned"
    (let ((deps (transitive-deps "~dep-island-with-chain" (test-env))))
      (assert-contains "~dep-branch" deps)
      (assert-contains "~dep-leaf" deps))))


;; --------------------------------------------------------------------------
;; 4. components-needed — page bundle computation
;; --------------------------------------------------------------------------

(defsuite "components-needed"

  (deftest "finds direct and transitive"
    (let ((needed (components-needed "(~dep-trunk)" (test-env))))
      (assert-contains "~dep-trunk" needed)
      (assert-contains "~dep-branch" needed)
      (assert-contains "~dep-leaf" needed)))

  (deftest "deduplicates"
    (let ((needed (components-needed "(div (~dep-leaf) (~dep-leaf))" (test-env))))
      ;; ~dep-leaf should appear only once
      (assert-true (contains? needed "~dep-leaf"))))

  (deftest "handles leaf page"
    (let ((needed (components-needed "(~dep-island)" (test-env))))
      (assert-contains "~dep-island" needed)
      (assert-equal 1 (len needed))))

  (deftest "handles multiple top-level components"
    (let ((needed (components-needed "(div (~dep-leaf) (~dep-island))" (test-env))))
      (assert-contains "~dep-leaf" needed)
      (assert-contains "~dep-island" needed)))

  (deftest "island deps included in page bundle"
    (let ((needed (components-needed "(~dep-island-with-chain)" (test-env))))
      (assert-contains "~dep-island-with-chain" needed)
      (assert-contains "~dep-branch" needed)
      (assert-contains "~dep-leaf" needed))))


;; --------------------------------------------------------------------------
;; 5. IO detection — scan-io-refs, component-pure?
;; --------------------------------------------------------------------------

;; Define components that reference "io" functions for testing
(defcomp ~dep-pure ()
  (div (~dep-leaf) "static"))

(defcomp ~dep-io ()
  (div (fetch-data "/api")))

(defcomp ~dep-io-indirect ()
  (div (~dep-io)))

(defsuite "scan-io-refs"

  (deftest "no IO in pure AST"
    (let ((refs (scan-io-refs (quote (div "hello" (span "world"))) (list "fetch-data"))))
      (assert-equal 0 (len refs))))

  (deftest "finds IO reference"
    (let ((refs (scan-io-refs (quote (div (fetch-data "/api"))) (list "fetch-data"))))
      (assert-contains "fetch-data" refs)))

  (deftest "multiple IO refs"
    (let ((refs (scan-io-refs (quote (div (fetch-data "/a") (query-db "x"))) (list "fetch-data" "query-db"))))
      (assert-contains "fetch-data" refs)
      (assert-contains "query-db" refs)))

  (deftest "ignores non-IO symbols"
    (let ((refs (scan-io-refs (quote (div (map str items))) (list "fetch-data"))))
      (assert-equal 0 (len refs)))))


(defsuite "component-pure?"

  (deftest "pure component is pure"
    (assert-true (component-pure? "~dep-pure" (test-env) (list "fetch-data"))))

  (deftest "IO component is not pure"
    (assert-false (component-pure? "~dep-io" (test-env) (list "fetch-data"))))

  (deftest "indirect IO is not pure"
    (assert-false (component-pure? "~dep-io-indirect" (test-env) (list "fetch-data"))))

  (deftest "leaf component is pure"
    (assert-true (component-pure? "~dep-leaf" (test-env) (list "fetch-data")))))


;; --------------------------------------------------------------------------
;; 6. render-target — boundary decision with affinity
;; --------------------------------------------------------------------------

;; Components with explicit affinity annotations
(defcomp ~dep-force-client (&key x)
  :affinity :client
  (div (fetch-data "/api") x))

(defcomp ~dep-force-server (&key x)
  :affinity :server
  (div x))

(defcomp ~dep-auto-pure (&key x)
  (div x))

(defcomp ~dep-auto-io (&key x)
  (div (fetch-data "/api")))

(defsuite "render-target"

  (deftest "pure auto component targets client"
    (assert-equal "client" (render-target "~dep-auto-pure" (test-env) (list "fetch-data"))))

  (deftest "IO auto component targets server"
    (assert-equal "server" (render-target "~dep-auto-io" (test-env) (list "fetch-data"))))

  (deftest "affinity client overrides IO to client"
    (assert-equal "client" (render-target "~dep-force-client" (test-env) (list "fetch-data"))))

  (deftest "affinity server overrides pure to server"
    (assert-equal "server" (render-target "~dep-force-server" (test-env) (list "fetch-data"))))

  (deftest "leaf component targets client"
    (assert-equal "client" (render-target "~dep-leaf" (test-env) (list "fetch-data"))))

  (deftest "unknown name targets server"
    (assert-equal "server" (render-target "~nonexistent" (test-env) (list "fetch-data")))))


;; --------------------------------------------------------------------------
;; 7. page-render-plan — per-page boundary plan
;; --------------------------------------------------------------------------

;; A page component that uses both pure and IO components
(defcomp ~plan-page (&key data)
  (div
    (~dep-auto-pure :x "hello")
    (~dep-auto-io :x data)
    (~dep-force-client :x "interactive")))

(defsuite "page-render-plan"

  (deftest "plan classifies components correctly"
    (let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
      ;; ~plan-page has transitive IO deps (via ~dep-auto-io) so targets server
      (assert-equal "server" (dict-get (get plan :components) "~plan-page"))
      (assert-equal "client" (dict-get (get plan :components) "~dep-auto-pure"))
      (assert-equal "server" (dict-get (get plan :components) "~dep-auto-io"))
      (assert-equal "client" (dict-get (get plan :components) "~dep-force-client"))))

  (deftest "plan server list contains IO components"
    (let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
      (assert-true (contains? (get plan :server) "~dep-auto-io"))))

  (deftest "plan client list contains pure components"
    (let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
      (assert-true (contains? (get plan :client) "~dep-auto-pure"))
      (assert-true (contains? (get plan :client) "~dep-force-client"))))

  (deftest "plan collects IO deps from server components"
    (let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
      (assert-true (contains? (get plan :io-deps) "fetch-data"))))

  (deftest "pure-only page has empty server list"
    (let ((plan (page-render-plan "(~dep-auto-pure :x 1)" (test-env) (list "fetch-data"))))
      (assert-equal 0 (len (get plan :server)))
      (assert-true (> (len (get plan :client)) 0)))))
