;; ==========================================================================
;; test-signals.sx — Tests for signals and reactive islands
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: signals.sx, eval.sx (defisland)
;;
;; Note: Multi-expression lambda bodies are wrapped in (do ...) for
;; compatibility with the hand-written evaluator which only supports
;; single-expression lambda bodies.
;; ==========================================================================


;; --------------------------------------------------------------------------
;; Signal creation and basic read/write
;; --------------------------------------------------------------------------

(defsuite "signal basics"
  (deftest "signal creates a reactive container"
    (let ((s (signal 42)))
      (assert-true (signal? s))
      (assert-equal 42 (deref s))))

  (deftest "deref on non-signal passes through"
    (assert-equal 5 (deref 5))
    (assert-equal "hello" (deref "hello"))
    (assert-nil (deref nil)))

  (deftest "reset! changes value"
    (let ((s (signal 0)))
      (reset! s 10)
      (assert-equal 10 (deref s))))

  (deftest "reset! does not notify when value unchanged"
    (let ((s (signal 5))
          (count (signal 0)))
      (effect (fn () (do (deref s) (swap! count inc))))
      ;; Effect runs once on creation → count=1
      (let ((c1 (deref count)))
        (reset! s 5)  ;; same value — no notification
        (assert-equal c1 (deref count)))))

  (deftest "swap! applies function to current value"
    (let ((s (signal 10)))
      (swap! s inc)
      (assert-equal 11 (deref s))))

  (deftest "swap! passes extra args"
    (let ((s (signal 10)))
      (swap! s + 5)
      (assert-equal 15 (deref s)))))


;; --------------------------------------------------------------------------
;; Computed signals
;; --------------------------------------------------------------------------

(defsuite "computed"
  (deftest "computed derives initial value"
    (let ((a (signal 3))
          (b (signal 4))
          (sum (computed (fn () (+ (deref a) (deref b))))))
      (assert-equal 7 (deref sum))))

  (deftest "computed updates when dependency changes"
    (let ((a (signal 2))
          (doubled (computed (fn () (* 2 (deref a))))))
      (assert-equal 4 (deref doubled))
      (reset! a 5)
      (assert-equal 10 (deref doubled))))

  (deftest "computed chains"
    (let ((base (signal 1))
          (doubled (computed (fn () (* 2 (deref base)))))
          (quadrupled (computed (fn () (* 2 (deref doubled))))))
      (assert-equal 4 (deref quadrupled))
      (reset! base 3)
      (assert-equal 12 (deref quadrupled)))))


;; --------------------------------------------------------------------------
;; Effects
;; --------------------------------------------------------------------------

(defsuite "effects"
  (deftest "effect runs immediately"
    (let ((ran (signal false)))
      (effect (fn () (reset! ran true)))
      (assert-true (deref ran))))

  (deftest "effect re-runs when dependency changes"
    (let ((source (signal "a"))
          (log (signal (list))))
      (effect (fn ()
        (swap! log (fn (l) (append l (deref source))))))
      ;; Initial run logs "a"
      (assert-equal (list "a") (deref log))
      ;; Change triggers re-run
      (reset! source "b")
      (assert-equal (list "a" "b") (deref log))))

  (deftest "effect dispose stops tracking"
    (let ((source (signal 0))
          (count (signal 0)))
      (let ((dispose (effect (fn () (do
            (deref source)
            (swap! count inc))))))
        ;; Effect ran once
        (assert-equal 1 (deref count))
        ;; Trigger
        (reset! source 1)
        (assert-equal 2 (deref count))
        ;; Dispose
        (dispose)
        ;; Should NOT trigger
        (reset! source 2)
        (assert-equal 2 (deref count)))))

  (deftest "effect cleanup runs before re-run"
    (let ((source (signal 0))
          (cleanups (signal 0)))
      (effect (fn () (do
        (deref source)
        (fn () (swap! cleanups inc)))))  ;; return cleanup fn
      ;; No cleanup yet (first run)
      (assert-equal 0 (deref cleanups))
      ;; Change triggers cleanup of previous run
      (reset! source 1)
      (assert-equal 1 (deref cleanups)))))


;; --------------------------------------------------------------------------
;; Batch
;; --------------------------------------------------------------------------

(defsuite "batch"
  (deftest "batch defers notifications"
    (let ((a (signal 0))
          (b (signal 0))
          (run-count (signal 0)))
      (effect (fn () (do
        (deref a) (deref b)
        (swap! run-count inc))))
      ;; Initial run
      (assert-equal 1 (deref run-count))
      ;; Without batch: 2 writes → 2 effect runs
      ;; With batch: 2 writes → 1 effect run
      (batch (fn () (do
        (reset! a 1)
        (reset! b 2))))
      ;; Should be 2 (initial + 1 batched), not 3
      (assert-equal 2 (deref run-count)))))


;; --------------------------------------------------------------------------
;; defisland
;; --------------------------------------------------------------------------

(defsuite "defisland"
  (deftest "defisland creates an island"
    (defisland ~test-island (&key value)
      (list "island" value))
    (assert-true (island? ~test-island)))

  (deftest "island is callable like component"
    (defisland ~greeting (&key name)
      (str "Hello, " name "!"))
    (assert-equal "Hello, World!" (~greeting :name "World")))

  (deftest "island accepts children"
    (defisland ~wrapper (&rest children)
      (list "wrap" children))
    (assert-equal (list "wrap" (list "a" "b"))
                  (~wrapper "a" "b"))))


;; --------------------------------------------------------------------------
;; Scope integration — reactive tracking uses scope-push!/scope-pop!
;; --------------------------------------------------------------------------

(defsuite "scope integration"
  (deftest "deref outside reactive scope does not subscribe"
    (let ((s (signal 42)))
      ;; Reading outside any reactive context should not add subscribers
      (assert-equal 42 (deref s))
      (assert-equal 0 (len (signal-subscribers s)))))

  (deftest "computed uses scope for tracking"
    (let ((a (signal 1))
          (b (signal 2))
          (sum (computed (fn () (+ (deref a) (deref b))))))
      ;; Each signal should have exactly 1 subscriber (the computed's recompute)
      (assert-equal 1 (len (signal-subscribers a)))
      (assert-equal 1 (len (signal-subscribers b)))
      ;; Verify computed value
      (assert-equal 3 (deref sum))))

  (deftest "nested effects with overlapping deps use scope correctly"
    (let ((shared (signal 0))
          (inner-only (signal 0))
          (outer-count (signal 0))
          (inner-count (signal 0)))
      ;; Outer effect tracks shared
      (effect (fn () (do (deref shared) (swap! outer-count inc))))
      ;; Inner effect tracks shared AND inner-only
      (effect (fn () (do (deref shared) (deref inner-only) (swap! inner-count inc))))
      ;; Both ran once
      (assert-equal 1 (deref outer-count))
      (assert-equal 1 (deref inner-count))
      ;; Changing shared triggers both
      (reset! shared 1)
      (assert-equal 2 (deref outer-count))
      (assert-equal 2 (deref inner-count))
      ;; Changing inner-only triggers only inner
      (reset! inner-only 1)
      (assert-equal 2 (deref outer-count))
      (assert-equal 3 (deref inner-count)))))
