;; interactive-debugger.sx — Condition debugger using *debugger-hook*
;;
;; Demonstrates the classic CL debugger pattern:
;;   - *debugger-hook* is invoked when an unhandled error reaches the top level
;;   - The hook receives the condition and a reference to itself
;;   - It can offer restarts interactively (here simulated with a policy fn)
;;
;; In real CL the debugger reads from the terminal. Here we simulate
;; the "user input" via a policy function passed in at call time.
;;
;; Depends on: lib/common-lisp/runtime.sx already loaded.

;; ── *debugger-hook* global ────────────────────────────────────────────────
;;
;; CL: when error is unhandled, invoke *debugger-hook* with (condition hook).
;; A nil hook means use the system default (which we simulate as re-raise).

(define cl-debugger-hook nil)

;; ── invoke-debugger ────────────────────────────────────────────────────────
;;
;; Called when cl-error finds no handler. Tries cl-debugger-hook first;
;; falls back to a simple error report.

(define
  cl-invoke-debugger
  (fn
    (c)
    (if
      (nil? cl-debugger-hook)
      (error (str "Debugger: " (cl-condition-message c)))
      (begin
        (let
          ((hook cl-debugger-hook))
          (set! cl-debugger-hook nil)
          (let
            ((result (hook c hook)))
            (set! cl-debugger-hook hook)
            result))))))

;; ── cl-error/debugger — error that routes through invoke-debugger ─────────

(define
  cl-error-with-debugger
  (fn
    (c &rest args)
    (let
      ((obj (cond ((cl-condition? c) c) ((string? c) (cl-make-condition "simple-error" "format-control" c "format-arguments" args)) (:else (cl-make-condition "simple-error" "format-control" (str c))))))
      (cl-signal-obj obj cl-handler-stack)
      (cl-invoke-debugger obj))))

;; ── simulated debugger session ────────────────────────────────────────────
;;
;; A debugger hook takes (condition hook) and "reads" user commands.
;; We simulate this with a policy function: (fn (c restarts) restart-name)
;; that picks a restart given the condition and available restarts.

(define
  make-policy-debugger
  (fn
    (policy)
    (fn
      (c hook)
      (let
        ((available (cl-compute-restarts)))
        (let
          ((choice (policy c available)))
          (if
            (and choice (not (nil? (cl-find-restart choice))))
            (cl-invoke-restart choice)
            (error
              (str
                "Debugger: no restart chosen for: "
                (cl-condition-message c)))))))))

;; ── tests ─────────────────────────────────────────────────────────────────

(define passed 0)
(define failed 0)
(define failures (list))

(define
  check
  (fn
    (label got expected)
    (if
      (= got expected)
      (set! passed (+ passed 1))
      (begin
        (set! failed (+ failed 1))
        (set!
          failures
          (append
            failures
            (list
              (str
                "FAIL ["
                label
                "]: got="
                (inspect got)
                " expected="
                (inspect expected)))))))))

(define
  reset-stacks!
  (fn
    ()
    (set! cl-handler-stack (list))
    (set! cl-restart-stack (list))
    (set! cl-debugger-hook nil)))

;; Test 1: debugger hook receives condition
(reset-stacks!)
(let
  ((received-msg ""))
  (begin
    (set!
      cl-debugger-hook
      (fn (c hook) (set! received-msg (cl-condition-message c)) nil))
    (cl-restart-case
      (fn () (cl-error-with-debugger "something broke"))
      (list "abort" (list) (fn () nil)))
    (check "debugger hook receives condition" received-msg "something broke")))

;; Test 2: policy-driven restart selection (use-zero)
(reset-stacks!)
(let
  ((result (begin (set! cl-debugger-hook (make-policy-debugger (fn (c restarts) "use-zero"))) (cl-restart-case (fn () (cl-error-with-debugger (cl-make-condition "division-by-zero")) 999) (list "use-zero" (list) (fn () 0))))))
  (check "policy debugger: use-zero restart" result 0))

;; Test 3: policy selects abort
(reset-stacks!)
(let
  ((result (begin (set! cl-debugger-hook (make-policy-debugger (fn (c restarts) "abort"))) (cl-restart-case (fn () (cl-error-with-debugger "aborting error") 999) (list "abort" (list) (fn () "aborted"))))))
  (check "policy debugger: abort restart" result "aborted"))

;; Test 4: compute-restarts inside debugger hook
(reset-stacks!)
(let
  ((seen-restarts (list)))
  (begin
    (set!
      cl-debugger-hook
      (fn
        (c hook)
        (set! seen-restarts (cl-compute-restarts))
        (cl-invoke-restart "continue")))
    (cl-restart-case
      (fn () (cl-error-with-debugger "test") 42)
      (list "continue" (list) (fn () "ok"))
      (list "abort" (list) (fn () "no")))
    (check
      "debugger: compute-restarts visible"
      (= (len seen-restarts) 2)
      true)))

;; Test 5: hook not invoked when handler catches first
(reset-stacks!)
(let
  ((hook-called false)
    (result
      (begin
        (set! cl-debugger-hook (fn (c hook) (set! hook-called true) nil))
        (cl-handler-case
          (fn () (cl-error-with-debugger "handled"))
          (list "error" (fn (c) "handler-won"))))))
  (check "handler wins; hook not called" hook-called false)
  (check "handler result returned" result "handler-won"))

;; Test 6: debugger-hook nil after re-raise guard
(reset-stacks!)
(let
  ((hook-calls 0))
  (begin
    (set!
      cl-debugger-hook
      (fn
        (c hook)
        (set! hook-calls (+ hook-calls 1))
        (if
          (> hook-calls 1)
          (error "infinite loop guard")
          (cl-invoke-restart "escape"))))
    (cl-restart-case
      (fn () (cl-error-with-debugger "once"))
      (list "escape" (list) (fn () nil)))
    (check
      "hook called exactly once (no infinite recursion)"
      hook-calls
      1)))

;; ── summary ────────────────────────────────────────────────────────────────

(define debugger-passed passed)
(define debugger-failed failed)
(define debugger-failures failures)