;; lib/acl/tests/explain.sx — Phase 3: proof correctness + audit completeness.

(define acl-et-pass 0)
(define acl-et-fail 0)
(define acl-et-failures (list))

;; Name-based deep equality. The host `=` compares symbols by interned
;; identity, which is unstable across substitution/saturation; comparing by
;; name (as the datalog suite does) makes structural assertions deterministic.
(define
  acl-et-eq?
  (fn
    (a b)
    (cond
      ((and (list? a) (list? b))
        (and (= (len a) (len b)) (acl-et-eq-l? a b 0)))
      ((and (dict? a) (dict? b))
        (let
          ((ka (keys a)) (kb (keys b)))
          (and (= (len ka) (len kb)) (acl-et-eq-d? a b ka 0))))
      ((and (symbol? a) (symbol? b))
        (= (symbol->string a) (symbol->string b)))
      (else (= a b)))))

(define
  acl-et-eq-l?
  (fn
    (a b i)
    (cond
      ((>= i (len a)) true)
      ((not (acl-et-eq? (nth a i) (nth b i))) false)
      (else (acl-et-eq-l? a b (+ i 1))))))

(define
  acl-et-eq-d?
  (fn
    (a b ka i)
    (cond
      ((>= i (len ka)) true)
      ((let ((k (nth ka i))) (not (acl-et-eq? (get a k) (get b k))))
        false)
      (else (acl-et-eq-d? a b ka (+ i 1))))))

(define
  acl-et-check!
  (fn
    (name got expected)
    (if
      (acl-et-eq? got expected)
      (set! acl-et-pass (+ acl-et-pass 1))
      (do
        (set! acl-et-fail (+ acl-et-fail 1))
        (append!
          acl-et-failures
          (str name "\n    expected: " expected "\n    got:      " got))))))

;; --- proof-tree walkers ---

;; True if EDB fact `target` appears as a base leaf anywhere in the proof.
(define
  acl-et-has-leaf?
  (fn
    (node target)
    (cond
      ((= node nil) false)
      ((and (dict? node) (has-key? node :via))
        (acl-et-eq? (get node :fact) target))
      ((and (dict? node) (has-key? node :body))
        (acl-et-any-leaf? (get node :body) target))
      (else false))))

(define
  acl-et-any-leaf?
  (fn
    (nodes target)
    (cond
      ((= (len nodes) 0) false)
      ((acl-et-has-leaf? (first nodes) target) true)
      (else (acl-et-any-leaf? (rest nodes) target)))))

;; True if the proof records a verified negation (deny did not fire).
(define
  acl-et-has-negok?
  (fn
    (node)
    (cond
      ((= node nil) false)
      ((and (dict? node) (has-key? node :neg-ok)) true)
      ((and (dict? node) (has-key? node :body))
        (acl-et-any-negok? (get node :body)))
      (else false))))

(define
  acl-et-any-negok?
  (fn
    (nodes)
    (cond
      ((= (len nodes) 0) false)
      ((acl-et-has-negok? (first nodes)) true)
      (else (acl-et-any-negok? (rest nodes))))))

(define
  acl-et-run-all!
  (fn
    ()
    (do
      (let
        ((db (acl-build-db (list (acl-grant (quote u) (quote read) (quote p))))))
        (let
          ((e (acl-explain db (quote u) (quote read) (quote p))))
          (do
            (acl-et-check! "direct: allowed?" (get e :allowed?) true)
            (acl-et-check!
              "direct: proof root fact"
              (get (get e :proof) :fact)
              (list (quote permit) (quote u) (quote read) (quote p)))
            (acl-et-check!
              "direct: grant leaf present"
              (acl-et-has-leaf?
                (get e :proof)
                (list (quote grant) (quote u) (quote read) (quote p)))
              true)
            (acl-et-check!
              "direct: negation verified"
              (acl-et-has-negok? (get e :proof))
              true)
            (acl-et-check!
              "direct: reason nil when allowed"
              (get e :reason)
              nil))))
      (let
        ((db (acl-build-db (list (acl-member-of (quote alice) (quote team)) (acl-member-of (quote team) (quote org)) (acl-grant (quote org) (quote read) (quote doc))))))
        (let
          ((e (acl-explain db (quote alice) (quote read) (quote doc))))
          (do
            (acl-et-check! "group: allowed?" (get e :allowed?) true)
            (acl-et-check!
              "group: member_of alice leaf"
              (acl-et-has-leaf?
                (get e :proof)
                (list (quote member_of) (quote alice) (quote team)))
              true)
            (acl-et-check!
              "group: member_of team leaf"
              (acl-et-has-leaf?
                (get e :proof)
                (list (quote member_of) (quote team) (quote org)))
              true)
            (acl-et-check!
              "group: grant org leaf at base"
              (acl-et-has-leaf?
                (get e :proof)
                (list (quote grant) (quote org) (quote read) (quote doc)))
              true))))
      (let
        ((db (acl-build-db (list (acl-child-of (quote sec) (quote book)) (acl-grant (quote u) (quote read) (quote book))))))
        (let
          ((e (acl-explain db (quote u) (quote read) (quote sec))))
          (do
            (acl-et-check! "resource: allowed?" (get e :allowed?) true)
            (acl-et-check!
              "resource: child_of leaf"
              (acl-et-has-leaf?
                (get e :proof)
                (list (quote child_of) (quote sec) (quote book)))
              true)
            (acl-et-check!
              "resource: grant on parent leaf"
              (acl-et-has-leaf?
                (get e :proof)
                (list (quote grant) (quote u) (quote read) (quote book)))
              true))))
      (let
        ((db (acl-build-db (list (acl-member-of (quote bob) (quote editor)) (acl-role-grant (quote editor) (quote edit) (quote page1))))))
        (let
          ((e (acl-explain db (quote bob) (quote edit) (quote page1))))
          (do
            (acl-et-check! "role: allowed?" (get e :allowed?) true)
            (acl-et-check!
              "role: member_of leaf"
              (acl-et-has-leaf?
                (get e :proof)
                (list (quote member_of) (quote bob) (quote editor)))
              true)
            (acl-et-check!
              "role: role_grant leaf"
              (acl-et-has-leaf?
                (get e :proof)
                (list
                  (quote role_grant)
                  (quote editor)
                  (quote edit)
                  (quote page1)))
              true))))
      (let
        ((db (acl-build-db (list (acl-grant (quote u) (quote edit) (quote p)) (acl-deny (quote u) (quote edit) (quote p))))))
        (let
          ((e (acl-explain db (quote u) (quote edit) (quote p))))
          (do
            (acl-et-check! "deny: not allowed" (get e :allowed?) false)
            (acl-et-check! "deny: no proof" (get e :proof) nil)
            (acl-et-check!
              "deny: reason root is eff_deny"
              (get (get e :reason) :fact)
              (list (quote eff_deny) (quote u) (quote edit) (quote p)))
            (acl-et-check!
              "deny: reason has deny leaf"
              (acl-et-has-leaf?
                (get e :reason)
                (list (quote deny) (quote u) (quote edit) (quote p)))
              true))))
      (let
        ((db (acl-build-db (list (acl-member-of (quote alice) (quote team)) (acl-grant (quote alice) (quote read) (quote doc)) (acl-deny (quote team) (quote read) (quote doc))))))
        (let
          ((e (acl-explain db (quote alice) (quote read) (quote doc))))
          (do
            (acl-et-check!
              "inherited deny: not allowed"
              (get e :allowed?)
              false)
            (acl-et-check!
              "inherited deny: reason has member_of leaf"
              (acl-et-has-leaf?
                (get e :reason)
                (list (quote member_of) (quote alice) (quote team)))
              true)
            (acl-et-check!
              "inherited deny: reason has group deny leaf"
              (acl-et-has-leaf?
                (get e :reason)
                (list (quote deny) (quote team) (quote read) (quote doc)))
              true))))
      (let
        ((db (acl-build-db (list))))
        (let
          ((e (acl-explain db (quote u) (quote read) (quote p))))
          (do
            (acl-et-check! "no grant: not allowed" (get e :allowed?) false)
            (acl-et-check! "no grant: proof nil" (get e :proof) nil)
            (acl-et-check! "no grant: reason nil" (get e :reason) nil))))
      (let
        ((db (acl-build-db (list (acl-grant (quote u) (quote read) (quote p)) (acl-deny (quote u) (quote edit) (quote p))))))
        (do
          (acl-audit-clear!)
          (acl-et-check! "audit: starts empty" (acl-audit-count) 0)
          (acl-et-check!
            "audit decide allowed returns true"
            (acl-audit-decide! db (quote u) (quote read) (quote p))
            true)
          (acl-et-check!
            "audit decide denied returns false"
            (acl-audit-decide! db (quote u) (quote edit) (quote p))
            false)
          (acl-audit-decide! db (quote u) (quote comment) (quote p))
          (acl-et-check!
            "audit: count after three decisions"
            (acl-audit-count)
            3)
          (acl-et-check!
            "audit: tail size respects n"
            (len (acl-audit-tail 2))
            2)
          (acl-et-check!
            "audit: tail returns most recent"
            (get (first (acl-audit-tail 1)) :act)
            (quote comment))
          (acl-et-check!
            "audit: first record seq is 0"
            (get (first (acl-audit-tail 3)) :seq)
            0)
          (acl-et-check!
            "audit: allowed flag recorded"
            (get (first (acl-audit-tail 3)) :allowed?)
            true)
          (acl-et-check!
            "audit: serialize line count"
            (len (acl-et-lines (acl-audit-serialize)))
            3)
          (acl-audit-clear!)
          (acl-et-check!
            "audit: clear resets count"
            (acl-audit-count)
            0))))))

;; count newline-terminated lines in a serialized log
(define acl-et-lines (fn (s) (acl-et-count-nl s 0 0)))
(define
  acl-et-count-nl
  (fn
    (s i n)
    (if
      (>= i (len s))
      (if (= n 0) (list) (acl-et-rangelist n))
      (acl-et-count-nl
        s
        (+ i 1)
        (if (= (slice s i (+ i 1)) "\n") (+ n 1) n)))))
(define
  acl-et-rangelist
  (fn
    (n)
    (if
      (<= n 0)
      (list)
      (cons n (acl-et-rangelist (- n 1))))))

(define
  acl-explain-tests-run!
  (fn
    ()
    (do
      (set! acl-et-pass 0)
      (set! acl-et-fail 0)
      (set! acl-et-failures (list))
      (acl-et-run-all!)
      {:failures acl-et-failures :total (+ acl-et-pass acl-et-fail) :passed acl-et-pass :failed acl-et-fail})))
