;; ==========================================================================
;; test-continuations-advanced.sx — Stress tests for multi-shot continuations
;;                                  and frame-based dynamic scope
;;
;; Requires: test-framework.sx loaded, continuations + scope extensions enabled.
;;
;; Tests the CEK continuation + ProvideFrame/ScopeAccFrame system under:
;;   - Multi-shot (k invoked 0, 1, 2, 3+ times)
;;   - Continuation composition across nested resets
;;   - provide/context: dynamic variable binding via kont walk
;;   - provide values preserved across shift/resume
;;   - scope/emit!/emitted: accumulator frames in kont
;;   - Accumulator frames preserved across shift/resume
;; ==========================================================================


;; --------------------------------------------------------------------------
;; 1. Multi-shot continuations
;; --------------------------------------------------------------------------

(defsuite "multi-shot-continuations"
  (deftest "k invoked 3 times returns list of results"
    ;; Each (k N) resumes (+ 1 N) independently.
    ;; Shift body collects all three results into a list.
    (assert-equal (list 11 21 31)
      (reset (+ 1 (shift k (list (k 10) (k 20) (k 30)))))))

  (deftest "k invoked via map over input list"
    ;; map applies k to each element; each resume computes (+ 1 elem).
    (assert-equal (list 11 21 31)
      (reset (+ 1 (shift k (map k (list 10 20 30)))))))

  (deftest "k invoked zero times — abort with plain value"
    ;; Shift body ignores k and returns 42 directly.
    ;; The outer (+ 1 ...) hole is never filled.
    (assert-equal 42
      (reset (+ 1 (shift k 42)))))

  (deftest "k invoked conditionally — true branch calls k"
    ;; Only the true branch calls k; result is (+ 1 10) = 11.
    (assert-equal 11
      (reset (+ 1 (shift k (if true (k 10) 99))))))

  (deftest "k invoked conditionally — false branch skips k"
    ;; False branch returns 99 directly without invoking k.
    (assert-equal 99
      (reset (+ 1 (shift k (if false (k 10) 99))))))

  (deftest "k invoked inside let binding"
    ;; (k 5) = (+ 1 5) = 6; x is bound to 6; (* x 2) = 12.
    (assert-equal 12
      (reset (+ 1 (shift k (let ((x (k 5))) (* x 2)))))))

  (deftest "nested shift — inner k2 called by outer k1"
    ;; k1 = (fn (v) (+ 1 v)), k2 = (fn (v) (+ 2 v))
    ;; (k2 3) = 5, (k1 5) = 6
    ;; inner reset returns 6 to shift-k1 body; (+ 10 6) = 16
    ;; outer reset returns 16
    (assert-equal 16
      (reset (+ 1 (shift k1 (+ 10 (reset (+ 2 (shift k2 (k1 (k2 3)))))))))))

  (deftest "k called twice accumulates both results"
    ;; Two invocations in a list: (k 1) = 2, (k 2) = 3.
    (assert-equal (list 2 3)
      (reset (+ 1 (shift k (list (k 1) (k 2)))))))

  (deftest "multi-shot k is idempotent — same arg gives same result"
    ;; Calling k with the same argument twice should yield equal values.
    (let ((results (reset (+ 1 (shift k (list (k 5) (k 5)))))))
      (assert-equal (nth results 0) (nth results 1)))))


;; --------------------------------------------------------------------------
;; 2. Continuation composition
;; --------------------------------------------------------------------------

(defsuite "continuation-composition"
  (deftest "two independent resets have isolated continuations"
    ;; Each reset is entirely separate — the two k values are unrelated.
    (let ((r1 (reset (+ 1 (shift k1 (k1 10)))))
          (r2 (reset (+ 100 (shift k2 (k2 5))))))
      (assert-equal 11  r1)
      (assert-equal 105 r2)))

  (deftest "continuation passed to helper function and invoked there"
    ;; apply-k is a plain lambda; it calls the continuation it receives.
    (let ((apply-k (fn (k v) (k v))))
      (assert-equal 15
        (reset (+ 5 (shift k (apply-k k 10)))))))

  (deftest "continuation stored in variable and invoked later"
    ;; reset returns k itself; we then invoke it outside the reset form.
    (let ((k (reset (shift k k))))
      ;; k = identity continuation for (reset _), so (k v) = v
      (assert-true (continuation? k))
      (assert-equal 42 (k 42))
      (assert-equal 7  (k 7))))

  (deftest "continuation stored then called with multiple values"
    ;; k from (+ 1 hole); invoking k with different args gives different results.
    (let ((k (reset (+ 1 (shift k k)))))
      (assert-equal 11 (k 10))
      (assert-equal 21 (k 20))
      (assert-equal 31 (k 30))))

  (deftest "continuation as argument to map — applied to a list"
    ;; k = (fn (v) (+ 10 v)); map applies it to each element.
    (let ((k (reset (+ 10 (shift k k)))))
      (assert-equal (list 11 12 13)
        (map k (list 1 2 3)))))

  (deftest "compose two continuations from nested resets"
    ;; k1 = (fn (v) (+ 1 v)), k2 = (fn (v) (+ 10 v))
    ;; (k2 0) = 10, (k1 10) = 11; outer reset returns 11.
    (assert-equal 11
      (reset (+ 1 (shift k1 (reset (+ 10 (shift k2 (k1 (k2 0))))))))))

  (deftest "continuation predicate holds inside and after capture"
    ;; k captured inside shift is a continuation; so is one returned by reset.
    (assert-true
      (reset (shift k (continuation? k))))
    (assert-true
      (continuation? (reset (shift k k))))))


;; --------------------------------------------------------------------------
;; 3. provide / context — basic dynamic scope
;; --------------------------------------------------------------------------

(defsuite "provide-context-basic"
  (deftest "simple provide and context"
    ;; (context \"x\") walks the kont and finds the ProvideFrame for \"x\".
    (assert-equal 42
      (provide "x" 42 (context "x"))))

  (deftest "nested provide — inner shadows outer"
    ;; The nearest ProvideFrame wins when searching kont.
    (assert-equal 2
      (provide "x" 1
        (provide "x" 2
          (context "x")))))

  (deftest "outer provide visible after inner scope exits"
    ;; After the inner provide's body finishes, its frame is gone.
    ;; The next (context \"x\") walks past it to the outer frame.
    (assert-equal 1
      (provide "x" 1
        (do
          (provide "x" 2 (context "x"))
          (context "x")))))

  (deftest "multiple provide names are independent"
    ;; Each name has its own ProvideFrame; they don't interfere.
    (assert-equal 3
      (provide "a" 1
        (provide "b" 2
          (+ (context "a") (context "b"))))))

  (deftest "context with default — provider present returns provided value"
    ;; Second arg to context is the default; present provider overrides it.
    (assert-equal 42
      (provide "x" 42 (context "x" 0))))

  (deftest "context with default — no provider returns default"
    ;; When no ProvideFrame exists for the name, the default is returned.
    (assert-equal 0
      (provide "y" 99 (context "x" 0))))

  (deftest "provide with computed value"
    ;; The value expression is evaluated before pushing the frame.
    (assert-equal 6
      (provide "n" (* 2 3) (context "n"))))

  (deftest "provide value is the exact bound value (no double-eval)"
    ;; Passing a list as the provided value should return that list.
    (let ((result (provide "items" (list 1 2 3) (context "items"))))
      (assert-equal (list 1 2 3) result))))


;; --------------------------------------------------------------------------
;; 4. provide across shift — scope survives continuation capture/resume
;; --------------------------------------------------------------------------

(defsuite "provide-across-shift"
  (deftest "provide value preserved across shift and k invocation"
    ;; The ProvideFrame lives in the kont beyond the ResetFrame.
    ;; When k resumes, the frame is still there — context finds it.
    (assert-equal "dark"
      (reset
        (provide "theme" "dark"
          (+ 0 (shift k (k 0)))
          (context "theme")))))

  (deftest "two provides both preserved across shift"
    ;; Both ProvideFrames must survive the shift/resume round-trip.
    (assert-equal 3
      (reset
        (provide "a" 1
          (provide "b" 2
            (+ 0 (shift k (k 0)))
            (+ (context "a") (context "b")))))))

  (deftest "context visible inside provide but not in shift body"
    ;; shift body runs OUTSIDE the reset boundary — provide is not in scope.
    ;; But context with a default should return the default.
    (assert-equal "fallback"
      (reset
        (provide "theme" "light"
          (shift k (context "theme" "fallback"))))))

  (deftest "context after k invocation restores scope frame"
    ;; k was captured with the ProvideFrame in its saved kont.
    ;; After (k v) resumes, context finds the frame again.
    (let ((result
            (reset
              (provide "color" "red"
                (+ 0 (shift k (k 0)))
                (context "color")))))
      (assert-equal "red" result)))

  (deftest "multi-shot: each k invocation reinstates captured ProvideFrame"
    ;; k captures the ProvideFrame for "n" (it's inside the reset delimiter).
    ;; Invoking k twice: each time (context "n") in the resumed body is valid.
    ;; The shift body collects (context "n") from each resumed branch.
    (let ((readings
            (reset
              (provide "n" 10
                (+ 0 (shift k
                       (list
                         (k 0)
                         (k 0))))
                (context "n")))))
      ;; Each (k 0) resumes and returns (context "n") = 10.
      (assert-equal (list 10 10) readings))))


;; --------------------------------------------------------------------------
;; 5. scope / emit! / emitted — accumulator frames
;; --------------------------------------------------------------------------

(defsuite "scope-emit-basic"
  (deftest "simple scope: emit two items and read emitted list"
    ;; emit! appends to the nearest ScopeAccFrame; emitted returns the list.
    (assert-equal (list "a" "b")
      (scope "css"
        (emit! "css" "a")
        (emit! "css" "b")
        (emitted "css"))))

  (deftest "empty scope returns empty list for emitted"
    ;; No emit! calls means the accumulator stays empty.
    (assert-equal (list)
      (scope "css"
        (emitted "css"))))

  (deftest "emit! order is preserved"
    ;; Items appear in emission order, not reverse.
    (assert-equal (list 1 2 3 4 5)
      (scope "nums"
        (emit! "nums" 1)
        (emit! "nums" 2)
        (emit! "nums" 3)
        (emit! "nums" 4)
        (emit! "nums" 5)
        (emitted "nums"))))

  (deftest "nested scopes: inner does not see outer's emitted"
    ;; The inner scope has its own ScopeAccFrame; kont-find-scope-acc
    ;; stops at the first matching name, so inner is fully isolated.
    (let ((inner-emitted
            (scope "css"
              (emit! "css" "outer")
              (scope "css"
                (emit! "css" "inner")
                (emitted "css")))))
      (assert-equal (list "inner") inner-emitted)))

  (deftest "two differently-named scopes are independent"
    ;; emit! to \"a\" must not appear in emitted \"b\" and vice versa.
    (let ((result-a nil) (result-b nil))
      (scope "a"
        (scope "b"
          (emit! "a" "for-a")
          (emit! "b" "for-b")
          (set! result-b (emitted "b")))
        (set! result-a (emitted "a")))
      (assert-equal (list "for-a") result-a)
      (assert-equal (list "for-b") result-b)))

  (deftest "scope body returns last expression value"
    ;; scope itself returns the last body expression, not the emitted list.
    (assert-equal 42
      (scope "x"
        (emit! "x" "ignored")
        42)))

  (deftest "scope with :value acts as provide for context"
    ;; When :value is given, the ScopeAccFrame also carries the value.
    ;; context should be able to read it (if the evaluator searches scope-acc
    ;; frames the same way as provide frames).
    ;; NOTE: this tests the :value keyword path in step-sf-scope.
    ;; If context only walks ProvideFrames, use provide directly instead.
    ;; We verify at minimum that :value does not crash.
    (let ((r (try-call (fn ()
                         (scope "x" :value 42
                           (emitted "x"))))))
      (assert-true (get r "ok")))))


;; --------------------------------------------------------------------------
;; 6. scope / emit! across shift — accumulator frames survive continuation
;; --------------------------------------------------------------------------

(defsuite "scope-emit-across-shift"
  (deftest "emit before and after shift both appear in emitted"
    ;; The ScopeAccFrame is in the kont beyond the ResetFrame.
    ;; After k resumes, the frame is still present; the second emit!
    ;; appends to it.
    (assert-equal (list "a" "b")
      (reset
        (scope "acc"
          (emit! "acc" "a")
          (+ 0 (shift k (k 0)))
          (emit! "acc" "b")
          (emitted "acc")))))

  (deftest "emit only before shift — one item in emitted"
    ;; emit! before shift commits to the frame; shift/resume preserves it.
    (assert-equal (list "only")
      (reset
        (scope "log"
          (emit! "log" "only")
          (+ 0 (shift k (k 0)))
          (emitted "log")))))

  (deftest "emit only after shift — one item in emitted"
    ;; No emit! before shift; the frame starts empty; post-resume emit! adds one.
    (assert-equal (list "after")
      (reset
        (scope "log"
          (+ 0 (shift k (k 0)))
          (emit! "log" "after")
          (emitted "log")))))

  (deftest "emits on both sides of single shift boundary"
    ;; Single shift/resume; emits before and after are preserved.
    (assert-equal (list "a" "b")
      (reset
        (scope "trace"
          (emit! "trace" "a")
          (+ 0 (shift k (k 0)))
          (emit! "trace" "b")
          (emitted "trace")))))

  (deftest "emitted inside shift body reads current accumulator"
    ;; kont in the shift body is rest-kont (outer kont beyond the reset).
    ;; The ScopeAccFrame should be present if it was installed before reset.
    ;; emit! and emitted inside shift body use that outer frame.
    (let ((outer-acc nil))
      (scope "outer"
        (reset
          (shift k
            (do
              (emit! "outer" "from-shift")
              (set! outer-acc (emitted "outer")))))
        nil)
      (assert-equal (list "from-shift") outer-acc))))

