;; lib/prolog/tests/parse.sx — parser unit tests
;;
;; Run: bash lib/prolog/tests/run-parse.sh
;; Or via sx-server: (load "lib/prolog/tokenizer.sx") (load "lib/prolog/parser.sx")
;;                   (load "lib/prolog/tests/parse.sx") (pl-parse-tests-run!)

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

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

;; Atoms & variables
(pl-test!
  "atom fact"
  (pl-parse "foo.")
  (list (list "clause" (list "atom" "foo") (list "atom" "true"))))

(pl-test! "number literal" (pl-parse-goal "42") (list "num" 42))

(pl-test!
  "negative number — not supported yet (parsed as op atom + num)"
  (pl-parse-goal "-5")
  (list "atom" "-"))

(pl-test! "variable" (pl-parse-goal "X") (list "var" "X"))

(pl-test!
  "underscore variable"
  (pl-parse-goal "_Ignored")
  (list "var" "_Ignored"))

(pl-test! "anonymous variable" (pl-parse-goal "_") (list "var" "_"))

(pl-test!
  "compound 1-arg"
  (pl-parse-goal "foo(a)")
  (list "compound" "foo" (list (list "atom" "a"))))

(pl-test!
  "compound 3-args mixed"
  (pl-parse-goal "p(X, 1, hello)")
  (list
    "compound"
    "p"
    (list (list "var" "X") (list "num" 1) (list "atom" "hello"))))

(pl-test!
  "nested compound"
  (pl-parse-goal "f(g(X), h(Y, Z))")
  (list
    "compound"
    "f"
    (list
      (list "compound" "g" (list (list "var" "X")))
      (list "compound" "h" (list (list "var" "Y") (list "var" "Z"))))))

;; Lists
(pl-test! "empty list" (pl-parse-goal "[]") (list "atom" "[]"))

(pl-test!
  "single-element list"
  (pl-parse-goal "[a]")
  (list "compound" "." (list (list "atom" "a") (list "atom" "[]"))))

(pl-test!
  "three-element list"
  (pl-parse-goal "[1, 2, 3]")
  (list
    "compound"
    "."
    (list
      (list "num" 1)
      (list
        "compound"
        "."
        (list
          (list "num" 2)
          (list "compound" "." (list (list "num" 3) (list "atom" "[]"))))))))

(pl-test!
  "head-tail list"
  (pl-parse-goal "[H|T]")
  (list "compound" "." (list (list "var" "H") (list "var" "T"))))

(pl-test!
  "two-head-tail list"
  (pl-parse-goal "[A, B|T]")
  (list
    "compound"
    "."
    (list
      (list "var" "A")
      (list "compound" "." (list (list "var" "B") (list "var" "T"))))))

;; Clauses
(pl-test!
  "fact"
  (pl-parse "parent(tom, bob).")
  (list
    (list
      "clause"
      (list
        "compound"
        "parent"
        (list (list "atom" "tom") (list "atom" "bob")))
      (list "atom" "true"))))

(pl-test!
  "rule with single-goal body"
  (pl-parse "q(X) :- p(X).")
  (list
    (list
      "clause"
      (list "compound" "q" (list (list "var" "X")))
      (list "compound" "p" (list (list "var" "X"))))))

(pl-test!
  "rule with conjunctive body"
  (pl-parse "r(X, Y) :- p(X), q(Y).")
  (list
    (list
      "clause"
      (list "compound" "r" (list (list "var" "X") (list "var" "Y")))
      (list
        "compound"
        ","
        (list
          (list "compound" "p" (list (list "var" "X")))
          (list "compound" "q" (list (list "var" "Y"))))))))

;; Cut in body
(pl-test!
  "cut in body"
  (pl-parse "foo(X) :- p(X), !, q(X).")
  (list
    (list
      "clause"
      (list "compound" "foo" (list (list "var" "X")))
      (list
        "compound"
        ","
        (list
          (list "compound" "p" (list (list "var" "X")))
          (list
            "compound"
            ","
            (list
              (list "cut")
              (list "compound" "q" (list (list "var" "X"))))))))))

;; Symbolic-atom compound terms (phase 1 form)
(pl-test!
  "= as compound"
  (pl-parse-goal "=(X, 5)")
  (list "compound" "=" (list (list "var" "X") (list "num" 5))))

(pl-test!
  "is with +"
  (pl-parse-goal "is(Y, +(X, 1))")
  (list
    "compound"
    "is"
    (list
      (list "var" "Y")
      (list "compound" "+" (list (list "var" "X") (list "num" 1))))))

;; Strings
(pl-test!
  "double-quoted string"
  (pl-parse-goal "\"hello\"")
  (list "str" "hello"))

;; Single-quoted atom
(pl-test!
  "quoted atom"
  (pl-parse-goal "'Hello World'")
  (list "atom" "Hello World"))

;; Multi-clause program
(pl-test!
  "append program"
  (len
    (pl-parse "append([], L, L).\nappend([H|T], L, [H|R]) :- append(T, L, R).\n"))
  2)

;; Comments
(pl-test!
  "line comment ignored"
  (pl-parse "foo.\n% this is a comment\nbar.")
  (list
    (list "clause" (list "atom" "foo") (list "atom" "true"))
    (list "clause" (list "atom" "bar") (list "atom" "true"))))

(pl-test!
  "block comment ignored"
  (pl-parse "/* hello */\nfoo.")
  (list (list "clause" (list "atom" "foo") (list "atom" "true"))))

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