;; lib/prolog/tests/assert_rules.sx — assert/assertz/asserta with rule terms (head :- body)
;; Tests that :- is in the op table (prec 1200 xfx) and pl-build-clause handles rule form.

(define pl-ar-test-count 0)
(define pl-ar-test-pass 0)
(define pl-ar-test-fail 0)
(define pl-ar-test-failures (list))

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

(define
  pl-ar-goal
  (fn
    (src env)
    (pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))

;; ── DB1: assertz a simple rule then query ──────────────────────────
(define pl-ar-db1 (pl-mk-db))

(pl-solve-once!
  pl-ar-db1
  (pl-ar-goal "assertz((double(X, Y) :- Y is X * 2))" {})
  (pl-mk-trail))

(pl-ar-test!
  "assertz rule: double(3, Y) succeeds"
  (pl-solve-once!
    pl-ar-db1
    (pl-ar-goal "double(3, Y)" {})
    (pl-mk-trail))
  true)

(define pl-ar-env1 {})
(pl-solve-once!
  pl-ar-db1
  (pl-ar-goal "double(3, Y)" pl-ar-env1)
  (pl-mk-trail))

(pl-ar-test!
  "assertz rule: double(3, Y) binds Y to 6"
  (pl-num-val (pl-walk-deep (dict-get pl-ar-env1 "Y")))
  6)

(define pl-ar-env1b {})
(pl-solve-once!
  pl-ar-db1
  (pl-ar-goal "double(10, Y)" pl-ar-env1b)
  (pl-mk-trail))

(pl-ar-test!
  "assertz rule: double(10, Y) yields 20"
  (pl-num-val (pl-walk-deep (dict-get pl-ar-env1b "Y")))
  20)

;; ── DB2: assert a rule with multiple facts, count solutions ─────────
(define pl-ar-db2 (pl-mk-db))

(pl-solve-once!
  pl-ar-db2
  (pl-ar-goal "assert(fact(a))" {})
  (pl-mk-trail))
(pl-solve-once!
  pl-ar-db2
  (pl-ar-goal "assert(fact(b))" {})
  (pl-mk-trail))
(pl-solve-once!
  pl-ar-db2
  (pl-ar-goal "assertz((copy(X) :- fact(X)))" {})
  (pl-mk-trail))

(pl-ar-test!
  "rule copy/1 using fact/1: 2 solutions"
  (pl-solve-count! pl-ar-db2 (pl-ar-goal "copy(X)" {}) (pl-mk-trail))
  2)

(define pl-ar-env2a {})
(pl-solve-once! pl-ar-db2 (pl-ar-goal "copy(X)" pl-ar-env2a) (pl-mk-trail))

(pl-ar-test!
  "rule copy/1: first solution is a"
  (pl-atom-name (pl-walk-deep (dict-get pl-ar-env2a "X")))
  "a")

;; ── DB3: asserta rule is tried before existing clauses ─────────────
(define pl-ar-db3 (pl-mk-db))

(pl-solve-once!
  pl-ar-db3
  (pl-ar-goal "assert(ord(a))" {})
  (pl-mk-trail))
(pl-solve-once!
  pl-ar-db3
  (pl-ar-goal "asserta((ord(b) :- true))" {})
  (pl-mk-trail))

(define pl-ar-env3 {})
(pl-solve-once! pl-ar-db3 (pl-ar-goal "ord(X)" pl-ar-env3) (pl-mk-trail))

(pl-ar-test!
  "asserta rule ord(b) is tried before ord(a)"
  (pl-atom-name (pl-walk-deep (dict-get pl-ar-env3 "X")))
  "b")

(pl-ar-test!
  "asserta: total solutions for ord/1 is 2"
  (pl-solve-count! pl-ar-db3 (pl-ar-goal "ord(X)" {}) (pl-mk-trail))
  2)

;; ── DB4: rule with conjunction in body ─────────────────────────────
(define pl-ar-db4 (pl-mk-db))

(pl-solve-once!
  pl-ar-db4
  (pl-ar-goal "assert(num(1))" {})
  (pl-mk-trail))
(pl-solve-once!
  pl-ar-db4
  (pl-ar-goal "assert(num(2))" {})
  (pl-mk-trail))
(pl-solve-once!
  pl-ar-db4
  (pl-ar-goal "assertz((big(X) :- num(X), X > 1))" {})
  (pl-mk-trail))

(pl-ar-test!
  "conjunction in rule body: big(1) fails"
  (pl-solve-once! pl-ar-db4 (pl-ar-goal "big(1)" {}) (pl-mk-trail))
  false)

(pl-ar-test!
  "conjunction in rule body: big(2) succeeds"
  (pl-solve-once! pl-ar-db4 (pl-ar-goal "big(2)" {}) (pl-mk-trail))
  true)

;; ── DB5: recursive rule ─────────────────────────────────────────────
(define pl-ar-db5 (pl-mk-db))

(pl-solve-once!
  pl-ar-db5
  (pl-ar-goal "assert((nat(0) :- true))" {})
  (pl-mk-trail))
(pl-solve-once!
  pl-ar-db5
  (pl-ar-goal "assertz((nat(s(X)) :- nat(X)))" {})
  (pl-mk-trail))

(pl-ar-test!
  "recursive rule: nat(0) succeeds"
  (pl-solve-once! pl-ar-db5 (pl-ar-goal "nat(0)" {}) (pl-mk-trail))
  true)

(pl-ar-test!
  "recursive rule: nat(s(0)) succeeds"
  (pl-solve-once!
    pl-ar-db5
    (pl-ar-goal "nat(s(0))" {})
    (pl-mk-trail))
  true)

(pl-ar-test!
  "recursive rule: nat(s(s(0))) succeeds"
  (pl-solve-once!
    pl-ar-db5
    (pl-ar-goal "nat(s(s(0)))" {})
    (pl-mk-trail))
  true)

(pl-ar-test!
  "recursive rule: nat(bad) fails"
  (pl-solve-once! pl-ar-db5 (pl-ar-goal "nat(bad)" {}) (pl-mk-trail))
  false)

;; ── DB6: rule with true body (explicit) ────────────────────────────
(define pl-ar-db6 (pl-mk-db))

(pl-solve-once!
  pl-ar-db6
  (pl-ar-goal "assertz((always(X) :- true))" {})
  (pl-mk-trail))
(pl-solve-once!
  pl-ar-db6
  (pl-ar-goal "assert(always(extra))" {})
  (pl-mk-trail))

(pl-ar-test!
  "rule body=true: always(foo) succeeds"
  (pl-solve-once!
    pl-ar-db6
    (pl-ar-goal "always(foo)" {})
    (pl-mk-trail))
  true)

(pl-ar-test!
  "rule body=true: always/1 has 2 clauses (1 rule + 1 fact)"
  (pl-solve-count!
    pl-ar-db6
    (pl-ar-goal "always(X)" {})
    (pl-mk-trail))
  2)

;; ── Runner ──────────────────────────────────────────────────────────
(define pl-assert-rules-tests-run! (fn () {:failed pl-ar-test-fail :passed pl-ar-test-pass :total pl-ar-test-count :failures pl-ar-test-failures}))
