;; lib/mod/tests/extensions.sx — beyond-roadmap extensions.
;;
;; Ext 1: negation-as-failure conditions (:not / :attr) + report attributes.
;;        "hide spam UNLESS the author is verified" (closed-world reasoning).
;; Ext 2: weighted/aggregate evidence scoring (:score-at-least) + report signals.
;;        Many low-confidence signals accumulate past a threshold via Prolog
;;        aggregate_all(sum(W), ...).
;; Ext 3: human-readable proof explanation (mod/explain) over the proof tree.
;; Demonstrated with custom rule sets so the default policy (and its conformance
;; tests) stays untouched.

(define mod-ext-count 0)
(define mod-ext-pass 0)
(define mod-ext-fail 0)
(define mod-ext-failures (list))

(define
  mod-ext-test!
  (fn
    (name got expected)
    (begin
      (set! mod-ext-count (+ mod-ext-count 1))
      (if
        (= got expected)
        (set! mod-ext-pass (+ mod-ext-pass 1))
        (begin
          (set! mod-ext-fail (+ mod-ext-fail 1))
          (append!
            mod-ext-failures
            (str name "\n    expected: " expected "\n    got:      " got)))))))

;; ── Ext 1: report attributes ──

(define mod-ext-r0 (mod/mk-report "r1" "a" "b" "this is spam"))
(mod-ext-test!
  "fresh report has no attrs"
  (len (mod/report-attrs mod-ext-r0))
  0)
(define mod-ext-rv (mod/attach-attr mod-ext-r0 "verified"))
(mod-ext-test!
  "attach-attr adds one attr"
  (len (mod/report-attrs mod-ext-rv))
  1)
(mod-ext-test!
  "attach-attr preserves evidence field"
  (len
    (mod/report-evidence
      (mod/attach-evidence mod-ext-rv (mod/mk-evidence "x" "y"))))
  1)
(mod-ext-test!
  "attach-evidence preserves attrs"
  (len
    (mod/report-attrs
      (mod/attach-evidence mod-ext-rv (mod/mk-evidence "x" "y"))))
  1)

;; ── Ext 1: negation-as-failure: spam hidden unless author verified ──

(define
  mod-ext-rules
  (list
    (mod/mk-rule
      "spam-unverified-hide"
      :hide (list
        (list :classification "spam")
        (list :not (list :attr "verified"))))
    (mod/mk-rule "default-keep" :keep (list))))

(define mod-ext-spam-plain (mod/mk-report "p1" "a" "b" "this is spam"))
(define
  mod-ext-spam-verified
  (mod/attach-attr (mod/mk-report "p2" "a" "b" "this is spam") "verified"))
(define mod-ext-clean (mod/mk-report "p3" "a" "b" "a fine post"))

(mod-ext-test!
  "unverified spam → hide"
  (get
    (mod/decide-report
      mod-ext-spam-plain
      (list mod-ext-spam-plain)
      mod-ext-rules)
    :action)
  "hide")
(mod-ext-test!
  "verified author spam → keep (negation blocks)"
  (get
    (mod/decide-report
      mod-ext-spam-verified
      (list mod-ext-spam-verified)
      mod-ext-rules)
    :action)
  "keep")
(mod-ext-test!
  "clean post → keep"
  (get
    (mod/decide-report mod-ext-clean (list mod-ext-clean) mod-ext-rules)
    :action)
  "keep")

;; ── Ext 1: negation appears in the goal text + proof ──

(define
  mod-ext-dec
  (mod/decide-report
    mod-ext-spam-plain
    (list mod-ext-spam-plain)
    mod-ext-rules))
(define mod-ext-goals (get (get mod-ext-dec :proof) :goals))

(mod-ext-test!
  "rule that matched is spam-unverified-hide"
  (get mod-ext-dec :rule)
  "spam-unverified-hide")
(mod-ext-test! "proof has two goals" (len mod-ext-goals) 2)
(mod-ext-test!
  "negation goal text"
  (get (nth mod-ext-goals 1) :goal)
  "not(attr(p1, verified))")
(mod-ext-test!
  "negation goal solved for unverified"
  (get (nth mod-ext-goals 1) :solved)
  true)

;; ── Ext 1: cond->goal compiles :attr and :not directly ──

(mod-ext-test!
  "cond->goal :attr"
  (mod/cond->goal (list :attr "verified") "Id")
  "attr(Id, verified)")
(mod-ext-test!
  "cond->goal :not wraps inner"
  (mod/cond->goal (list :not (list :classification "spam")) "Id")
  "not(classification(Id, spam))")

;; ── Ext 1: positive :attr condition (allowlist-style) ──

(define
  mod-ext-allow-rules
  (list
    (mod/mk-rule "trusted-keep" :keep (list (list :attr "trusted")))
    (mod/mk-rule "spam-hide" :hide (list (list :classification "spam")))
    (mod/mk-rule "default-keep" :keep (list))))

(define
  mod-ext-trusted-spam
  (mod/attach-attr (mod/mk-report "t1" "a" "b" "this is spam") "trusted"))
(mod-ext-test!
  "trusted attr exempts spam → keep"
  (get
    (mod/decide-report
      mod-ext-trusted-spam
      (list mod-ext-trusted-spam)
      mod-ext-allow-rules)
    :action)
  "keep")

;; ── Ext 2: weighted signals + aggregate scoring ──

(define mod-ext-s0 (mod/mk-report "s1" "a" "b" "neutral"))
(mod-ext-test!
  "fresh report has no signals"
  (len (mod/report-signals mod-ext-s0))
  0)
(define
  mod-ext-s1
  (mod/attach-signal mod-ext-s0 (mod/mk-signal "link" 2)))
(mod-ext-test!
  "attach-signal adds one"
  (len (mod/report-signals mod-ext-s1))
  1)
(mod-ext-test!
  "attach-signal preserves attrs"
  (len
    (mod/report-attrs
      (mod/attach-signal mod-ext-rv (mod/mk-signal "x" 1))))
  1)

(define
  mod-ext-score-rules
  (list
    (mod/mk-rule
      "high-score-hide"
      :hide (list (list :score-at-least 5)))
    (mod/mk-rule "default-keep" :keep (list))))

;; one weak signal (2) — below threshold
(define
  mod-ext-weak
  (mod/attach-signal
    (mod/mk-report "w1" "a" "b" "neutral")
    (mod/mk-signal "link" 2)))
(mod-ext-test!
  "single weak signal → keep (below threshold)"
  (get
    (mod/decide-report mod-ext-weak (list mod-ext-weak) mod-ext-score-rules)
    :action)
  "keep")

;; three signals summing to 6 — over threshold
(define
  mod-ext-strong0
  (mod/attach-signal
    (mod/mk-report "w2" "a" "b" "neutral")
    (mod/mk-signal "link" 2)))
(define
  mod-ext-strong1
  (mod/attach-signal mod-ext-strong0 (mod/mk-signal "newaccount" 2)))
(define
  mod-ext-strong
  (mod/attach-signal mod-ext-strong1 (mod/mk-signal "burst" 2)))
(mod-ext-test!
  "accumulated signals (2+2+2=6) → hide"
  (get
    (mod/decide-report
      mod-ext-strong
      (list mod-ext-strong)
      mod-ext-score-rules)
    :action)
  "hide")
(mod-ext-test!
  "scoring rule named in decision"
  (get
    (mod/decide-report
      mod-ext-strong
      (list mod-ext-strong)
      mod-ext-score-rules)
    :rule)
  "high-score-hide")

;; exactly at threshold (5) fires
(define
  mod-ext-exact0
  (mod/attach-signal
    (mod/mk-report "w3" "a" "b" "neutral")
    (mod/mk-signal "link" 3)))
(define
  mod-ext-exact
  (mod/attach-signal mod-ext-exact0 (mod/mk-signal "burst" 2)))
(mod-ext-test!
  "exactly at threshold (5) → hide"
  (get
    (mod/decide-report mod-ext-exact (list mod-ext-exact) mod-ext-score-rules)
    :action)
  "hide")

(mod-ext-test!
  "cond->goal :score-at-least"
  (mod/cond->goal (list :score-at-least 5) "Id")
  "aggregate_all(sum(W), signal(Id, _, W), T), T >= 5")

;; ── Ext 3: human-readable proof explanation ──

(define mod-ext-spam-explain (mod/explain mod-ext-dec))

(mod-ext-test!
  "explain mentions the report id"
  (mod/str-contains? mod-ext-spam-explain "Report p1")
  true)
(mod-ext-test!
  "explain mentions the action"
  (mod/str-contains? mod-ext-spam-explain "hide")
  true)
(mod-ext-test!
  "explain mentions the rule"
  (mod/str-contains? mod-ext-spam-explain "spam-unverified-hide")
  true)
(mod-ext-test!
  "explain marks proved goals"
  (mod/str-contains? mod-ext-spam-explain "[proved]")
  true)
(mod-ext-test!
  "explain renders the evidence line"
  (mod/str-contains? mod-ext-spam-explain "Evidence: spam")
  true)

;; count-rule explanation shows the unification bindings
(define mod-ext-rep-r (mod/mk-report "rc" "ann" "dave" "off-topic"))
(define
  mod-ext-rep-d
  (mod/decide-report
    mod-ext-rep-r
    (list mod-ext-rep-r mod-ext-rep-r mod-ext-rep-r)
    mod/default-rules))
(define mod-ext-rep-explain (mod/explain mod-ext-rep-d))
(mod-ext-test!
  "explain shows binding N=3"
  (mod/str-contains? mod-ext-rep-explain "N=3")
  true)
(mod-ext-test!
  "explain shows subject binding"
  (mod/str-contains? mod-ext-rep-explain "dave")
  true)

;; explain-goal direct: unproved goal gets [unproved]
(mod-ext-test!
  "explain-goal marks unproved"
  (mod/str-contains? (mod/explain-goal {:solved false :goal "attr(x, foo)" :bindings {}}) "[unproved]")
  true)
;; explain-binds renders key=value pairs
(mod-ext-test!
  "explain-binds renders pair"
  (mod/explain-binds {:N "3"})
  "N=3")
;; no-evidence decision says (none)
(define
  mod-ext-keep-d
  (mod/decide-report mod-ext-clean (list mod-ext-clean) mod-ext-rules))
(mod-ext-test!
  "explain (none) for empty evidence"
  (mod/str-contains? (mod/explain mod-ext-keep-d) "Evidence: (none)")
  true)

(define mod-extensions-tests-run! (fn () {:failures mod-ext-failures :total mod-ext-count :passed mod-ext-pass :failed mod-ext-fail}))
