;; lib/flow/tests/combinators.sx — Phase 5: combinator library (tap, recover, map-flow, iteration).

(define flow-cmb-pass 0)
(define flow-cmb-fail 0)
(define flow-cmb-fails (list))

(define
  flow-cmb-test
  (fn
    (name actual expected)
    (if
      (= actual expected)
      (set! flow-cmb-pass (+ flow-cmb-pass 1))
      (begin
        (set! flow-cmb-fail (+ flow-cmb-fail 1))
        (append! flow-cmb-fails {:name name :expected expected :actual actual})))))

(define flow-m (fn (src) (flow-run src)))

;; ── tap (side-effecting pass-through) ───────────────────────────
(flow-cmb-test
  "tap: returns input unchanged"
  (flow-m "(flow/start (tap (lambda (x) (* x 999))) 7)")
  7)
(flow-cmb-test
  "tap: runs the side effect"
  (flow-m
    "(define seen 0) (flow/start (tap (lambda (x) (set! seen x))) 42) seen")
  42)
(flow-cmb-test
  "tap: value flows on while the effect observes it"
  (flow-m
    "(define log 0) (flow/start (sequence (lambda (x) (+ x 1)) (tap (lambda (x) (set! log x))) (lambda (x) (* x 2))) 10) (list log (flow/result 1))")
  (list 11 22))

;; ── recover (fail-value counterpart of try-catch) ───────────────
(flow-cmb-test
  "recover: passes a non-fail value through"
  (flow-m "(flow/start (recover (lambda (x) (* x 2)) (lambda (r) -1)) 5)")
  10)
(flow-cmb-test
  "recover: handles a fail value via the reason"
  (flow-m
    "(flow/start (recover (lambda (x) (fail (quote too-small))) (lambda (r) (list (quote recovered) r))) 1)")
  (list "recovered" "too-small"))
(flow-cmb-test
  "recover: handler can supply a default value"
  (flow-m
    "(flow/start (sequence (recover (lambda (x) (if (> x 0) x (fail (quote neg))) ) (flow-const 0)) (lambda (x) (* x 10))) -3)")
  0)
(flow-cmb-test
  "recover: does not catch raised exceptions (those are try-catch's job)"
  (flow-m
    "(flow/start (try-catch (recover (lambda (x) (raise (quote boom))) (flow-const 0)) (lambda (e) e)) 1)")
  "boom")

;; ── map-flow (run a node over a list, join) ─────────────────────
(flow-cmb-test
  "map-flow: applies the node to each item"
  (flow-m "(flow/start (map-flow (lambda (x) (* x x))) (list 1 2 3 4))")
  (list 1 4 9 16))
(flow-cmb-test
  "map-flow: empty list joins to empty"
  (flow-m "(flow/start (map-flow (lambda (x) (+ x 1))) (list))")
  (list))
(flow-cmb-test
  "map-flow: each item runs an independent sub-flow"
  (flow-m
    "(flow/start (map-flow (sequence (lambda (x) (+ x 1)) (lambda (x) (* x 2)))) (list 0 4 9))")
  (list 2 10 20))
(flow-cmb-test
  "map-flow: composes — fan over a list then reduce the join"
  (flow-m
    "(flow/start (sequence (map-flow (lambda (x) (* x 10))) (lambda (xs) (apply + xs))) (list 1 2 3))")
  60)

;; ── flow-while / flow-until (bounded iteration) ─────────────────
(flow-cmb-test
  "flow-while: iterates while the predicate holds"
  (flow-m
    "(flow/start (flow-while (lambda (x) (< x 10)) (lambda (x) (+ x 1)) 100) 0)")
  10)
(flow-cmb-test
  "flow-while: a false predicate leaves input unchanged"
  (flow-m
    "(flow/start (flow-while (lambda (x) (< x 0)) (lambda (x) (+ x 1)) 100) 5)")
  5)
(flow-cmb-test
  "flow-while: respects the max-iteration bound"
  (flow-m "(flow/start (flow-while (lambda (x) #t) (lambda (x) (+ x 1)) 3) 0)")
  3)
(flow-cmb-test
  "flow-while: doubles until past a threshold"
  (flow-m
    "(flow/start (flow-while (lambda (x) (< x 50)) (lambda (x) (* x 2)) 100) 3)")
  96)
(flow-cmb-test
  "flow-until: iterates until the predicate becomes true"
  (flow-m
    "(flow/start (flow-until (lambda (x) (>= x 10)) (lambda (x) (+ x 3)) 100) 0)")
  12)
(flow-cmb-test
  "flow-until: composes inside a sequence"
  (flow-m
    "(flow/start (sequence (flow-until (lambda (x) (> x 100)) (lambda (x) (* x 3)) 100) (lambda (x) (- x 100))) 5)")
  35)

(define flow-cmb-tests-run! (fn () {:total (+ flow-cmb-pass flow-cmb-fail) :passed flow-cmb-pass :failed flow-cmb-fail :fails flow-cmb-fails}))
