;; lib/flow/tests/control.sx — Phase 2: control flow + error handling.

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

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

(define flow-c (fn (src) (flow-run src)))
(define flow-cs (fn (src) (get (flow-run src) :scm-string)))

;; ── branch ──────────────────────────────────────────────────────
(flow-ctl-test
  "branch: true selects then"
  (flow-c
    "(flow/start (branch (lambda (x) (> x 0)) (lambda (x) (* x 100)) (lambda (x) (- 0 x))) 5)")
  500)
(flow-ctl-test
  "branch: false selects else"
  (flow-c
    "(flow/start (branch (lambda (x) (> x 0)) (lambda (x) (* x 100)) (lambda (x) (- 0 x))) -3)")
  3)
(flow-ctl-test
  "branch: predicate sees the threaded input"
  (flow-c
    "(flow/start (sequence (lambda (x) (+ x 1)) (branch (lambda (x) (> x 3)) (flow-const 100) (flow-const 0))) 3)")
  100)
(flow-ctl-test
  "branch: branches are full nodes (sequence inside)"
  (flow-c
    "(flow/start (branch (lambda (x) (< x 10)) (sequence (lambda (x) (+ x 1)) (lambda (x) (* x 2))) (flow-const 0)) 4)")
  10)
(flow-ctl-test
  "branch: nested branch (3-way sign)"
  (flow-c
    "(defflow sign (branch (lambda (x) (> x 0)) (flow-const 1) (branch (lambda (x) (< x 0)) (flow-const -1) (flow-const 0)))) (list (flow/start sign 7) (flow/start sign -7) (flow/start sign 0))")
  (list 1 -1 0))
(flow-ctl-test
  "branch: publish-shaped approval gate"
  (flow-cs
    "(defflow publish (branch (lambda (post) (>= (string-length post) 3)) (lambda (post) (string-append post \" [published]\")) (lambda (post) (string-append post \" [rejected]\")))) (flow/start publish \"ok\")")
  "ok [rejected]")

;; ── error model — explicit (fail reason) values ─────────────────
(flow-ctl-test
  "fail: failed? is true for a failure value"
  (flow-c "(failed? (fail 404))")
  true)
(flow-ctl-test
  "fail: fail-reason extracts the reason"
  (flow-c "(fail-reason (fail 404))")
  404)
(flow-ctl-test
  "fail: failed? is false for a plain value"
  (flow-c "(failed? 7)")
  false)
(flow-ctl-test
  "fail: failed? is false for an ordinary list"
  (flow-c "(failed? (list 1 2 3))")
  false)
(flow-ctl-test
  "fail: a node may emit a failure as data"
  (flow-c
    "(defflow validate (lambda (s) (if (>= (string-length s) 3) s (fail (quote too-short))))) (failed? (flow/start validate \"hi\"))")
  true)
(flow-ctl-test
  "fail: failure flows downstream, branch recovers"
  (flow-c
    "(defflow guarded (sequence (lambda (s) (if (>= (string-length s) 3) (string-length s) (fail (quote too-short)))) (branch failed? (lambda (f) (list (quote recovered) (fail-reason f))) (lambda (n) (list (quote ok) n))))) (flow/start guarded \"hi\")")
  (list "recovered" "too-short"))

;; ── try-catch — reify raised exceptions ─────────────────────────
(flow-ctl-test
  "try-catch: no exception returns node result"
  (flow-c "(flow/start (try-catch (lambda (x) (* x 2)) (lambda (e) -1)) 5)")
  10)
(flow-ctl-test
  "try-catch: handler runs on raise"
  (flow-c
    "(flow/start (try-catch (lambda (x) (raise (quote boom))) (flow-const 99)) 1)")
  99)
(flow-ctl-test
  "try-catch: handler receives the reified error"
  (flow-c "(flow/start (try-catch (lambda (x) (raise 42)) (lambda (e) e)) 0)")
  42)
(flow-ctl-test
  "try-catch: catches exception from deep inside a sequence"
  (flow-c
    "(flow/start (try-catch (sequence (lambda (x) (+ x 1)) (lambda (x) (raise (quote deep)))) (flow-const -99)) 5)")
  -99)
(flow-ctl-test
  "try-catch: handler may convert to a failure value"
  (flow-c
    "(failed? (flow/start (try-catch (lambda (x) (raise (quote bad))) (lambda (e) (fail e))) 0))")
  true)
(flow-ctl-test
  "try-catch: composes — recover then continue"
  (flow-c
    "(flow/start (sequence (try-catch (lambda (x) (raise (quote x))) (flow-const 10)) (lambda (n) (* n 5))) 0)")
  50)

;; ── retry — re-run on raised exceptions ─────────────────────────
(flow-ctl-test
  "retry: succeeds after transient failures"
  (flow-c
    "(define ctr 0) (defflow flaky (lambda (x) (set! ctr (+ ctr 1)) (if (< ctr 3) (raise (quote nope)) (* x 10)))) (list (flow/start (retry 5 flaky) 7) ctr)")
  (list 70 3))
(flow-ctl-test
  "retry: exhausted re-raises (caught by try-catch)"
  (flow-c
    "(flow/start (try-catch (retry 2 (lambda (x) (raise (quote always)))) (flow-const (quote gaveup))) 0)")
  "gaveup")
(flow-ctl-test
  "retry: n=1 means a single attempt"
  (flow-c
    "(define ctr 0) (flow/start (try-catch (retry 1 (lambda (x) (set! ctr (+ ctr 1)) (raise (quote bad)))) (lambda (e) ctr)) 0)")
  1)
(flow-ctl-test
  "retry: success on first attempt does not re-run"
  (flow-c
    "(define ctr 0) (flow/start (sequence (retry 5 (lambda (x) (set! ctr (+ ctr 1)) (* x 2))) (lambda (n) ctr)) 21)")
  1)
(flow-ctl-test
  "retry: does not retry explicit failure values"
  (flow-c
    "(define ctr 0) (failed? (flow/start (retry 5 (lambda (x) (set! ctr (+ ctr 1)) (fail (quote bad)))) 0))")
  true)
(flow-ctl-test
  "retry: failure-value path runs node exactly once"
  (flow-c
    "(define ctr 0) (flow/start (sequence (retry 5 (lambda (x) (set! ctr (+ ctr 1)) (fail (quote bad)))) (lambda (f) ctr)) 0)")
  1)

;; ── timeout — cooperative step budget ───────────────────────────
(flow-ctl-test
  "timeout: work within budget completes"
  (flow-c
    "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 10 (lambda (x) (cd x))) (flow-const (quote timed-out))) 5)")
  99)
(flow-ctl-test
  "timeout: work exceeding budget raises flow-timeout"
  (flow-c
    "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 10 (lambda (x) (cd x))) (flow-const (quote timed-out))) 20)")
  "timed-out")
(flow-ctl-test
  "timeout: exact budget boundary completes"
  (flow-c
    "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 5 (lambda (x) (cd x))) (flow-const (quote timed-out))) 5)")
  99)
(flow-ctl-test
  "timeout: one tick over the budget raises"
  (flow-c
    "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 5 (lambda (x) (cd x))) (flow-const (quote timed-out))) 6)")
  "timed-out")
(flow-ctl-test
  "timeout: the raised error is identifiable"
  (flow-c
    "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 2 (lambda (x) (cd x))) (lambda (e) e)) 9)")
  "flow-timeout")
(flow-ctl-test
  "timeout: a node that never ticks is unbounded"
  (flow-c "(flow/start (timeout 0 (lambda (x) (* x 2))) 5)")
  10)
(flow-ctl-test
  "timeout: budget is restored across sequential timeouts"
  (flow-c
    "(define (cd n) (if (<= n 0) 1 (begin (tick) (cd (- n 1))))) (flow/start (sequence (timeout 4 (lambda (x) (cd x))) (timeout 4 (lambda (x) (cd 3))) (lambda (x) (begin (tick) (+ x 100)))) 3)")
  101)

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