;; Common Lisp reader/parser tests

(define cl-test-pass 0)
(define cl-test-fail 0)
(define cl-test-fails (list))

(define
  cl-deep=
  (fn
    (a b)
    (cond
      ((= a b) true)
      ((and (dict? a) (dict? b))
        (let
          ((ak (keys a)) (bk (keys b)))
          (if
            (not (= (len ak) (len bk)))
            false
            (every?
              (fn (k) (and (has-key? b k) (cl-deep= (get a k) (get b k))))
              ak))))
      ((and (list? a) (list? b))
        (if
          (not (= (len a) (len b)))
          false
          (let
            ((i 0) (ok true))
            (define
              chk
              (fn
                ()
                (when
                  (and ok (< i (len a)))
                  (do
                    (when
                      (not (cl-deep= (nth a i) (nth b i)))
                      (set! ok false))
                    (set! i (+ i 1))
                    (chk)))))
            (chk)
            ok)))
      (:else false))))

(define
  cl-test
  (fn
    (name actual expected)
    (if
      (cl-deep= actual expected)
      (set! cl-test-pass (+ cl-test-pass 1))
      (do
        (set! cl-test-fail (+ cl-test-fail 1))
        (append! cl-test-fails {:name name :expected expected :actual actual})))))

;; ── atoms ─────────────────────────────────────────────────────────

(cl-test "integer: 42" (cl-read "42") 42)
(cl-test "integer: 0" (cl-read "0") 0)
(cl-test "integer: negative" (cl-read "-5") -5)
(cl-test "integer: positive sign" (cl-read "+3") 3)
(cl-test "integer: hex #xFF" (cl-read "#xFF") 255)
(cl-test "integer: hex #xAB" (cl-read "#xAB") 171)
(cl-test "integer: binary #b1010" (cl-read "#b1010") 10)
(cl-test "integer: octal #o17" (cl-read "#o17") 15)

(cl-test "float: type" (get (cl-read "3.14") "cl-type") "float")
(cl-test "float: value" (get (cl-read "3.14") "value") "3.14")
(cl-test "float: neg" (get (cl-read "-2.5") "value") "-2.5")
(cl-test "float: exp" (get (cl-read "1.0e10") "value") "1.0e10")

(cl-test "ratio: type" (get (cl-read "1/3") "cl-type") "ratio")
(cl-test "ratio: value" (get (cl-read "1/3") "value") "1/3")
(cl-test "ratio: 22/7" (get (cl-read "22/7") "value") "22/7")

(cl-test "string: basic" (cl-read "\"hello\"") {:cl-type "string" :value "hello"})
(cl-test "string: empty" (cl-read "\"\"") {:cl-type "string" :value ""})
(cl-test "string: with escape" (cl-read "\"a\\nb\"") {:cl-type "string" :value "a\nb"})

(cl-test "symbol: foo" (cl-read "foo") "FOO")
(cl-test "symbol: BAR" (cl-read "BAR") "BAR")
(cl-test "symbol: pkg:sym" (cl-read "cl:car") "CL:CAR")
(cl-test "symbol: pkg::sym" (cl-read "pkg::foo") "PKG::FOO")

(cl-test "nil: symbol" (cl-read "nil") nil)
(cl-test "nil: uppercase" (cl-read "NIL") nil)
(cl-test "t: symbol" (cl-read "t") true)
(cl-test "t: uppercase" (cl-read "T") true)

(cl-test "keyword: type" (get (cl-read ":foo") "cl-type") "keyword")
(cl-test "keyword: name" (get (cl-read ":foo") "name") "FOO")
(cl-test "keyword: :test" (get (cl-read ":test") "name") "TEST")

(cl-test "char: type" (get (cl-read "#\\a") "cl-type") "char")
(cl-test "char: value" (get (cl-read "#\\a") "value") "a")
(cl-test "char: Space" (get (cl-read "#\\Space") "value") " ")
(cl-test "char: Newline" (get (cl-read "#\\Newline") "value") "\n")

(cl-test "uninterned: type" (get (cl-read "#:foo") "cl-type") "uninterned")
(cl-test "uninterned: name" (get (cl-read "#:foo") "name") "FOO")

;; ── lists ─────────────────────────────────────────────────────────

(cl-test "list: empty" (cl-read "()") (list))
(cl-test "list: one element" (cl-read "(foo)") (list "FOO"))
(cl-test "list: two elements" (cl-read "(foo bar)") (list "FOO" "BAR"))
(cl-test "list: nested" (cl-read "((a b) c)") (list (list "A" "B") "C"))
(cl-test "list: with integer" (cl-read "(+ 1 2)") (list "+" 1 2))
(cl-test "list: with string" (cl-read "(print \"hi\")") (list "PRINT" {:cl-type "string" :value "hi"}))
(cl-test "list: nil element" (cl-read "(a nil b)") (list "A" nil "B"))
(cl-test "list: t element" (cl-read "(a t b)") (list "A" true "B"))

;; ── dotted pairs ──────────────────────────────────────────────��──

(cl-test "dotted: type" (get (cl-read "(a . b)") "cl-type") "cons")
(cl-test "dotted: car" (get (cl-read "(a . b)") "car") "A")
(cl-test "dotted: cdr" (get (cl-read "(a . b)") "cdr") "B")
(cl-test "dotted: number cdr" (get (cl-read "(x . 42)") "cdr") 42)

;; ── reader macros ────────────────────────────────────────────────��

(cl-test "quote: form" (cl-read "'x") (list "QUOTE" "X"))
(cl-test "quote: list" (cl-read "'(a b)") (list "QUOTE" (list "A" "B")))
(cl-test "backquote: form" (cl-read "`x") (list "QUASIQUOTE" "X"))
(cl-test "unquote: form" (cl-read ",x") (list "UNQUOTE" "X"))
(cl-test "comma-at: form" (cl-read ",@x") (list "UNQUOTE-SPLICING" "X"))
(cl-test "function: form" (cl-read "#'foo") (list "FUNCTION" "FOO"))

;; ── vector ────────────────────────────────────────────────────────

(cl-test "vector: type" (get (cl-read "#(1 2 3)") "cl-type") "vector")
(cl-test "vector: elements" (get (cl-read "#(1 2 3)") "elements") (list 1 2 3))
(cl-test "vector: empty" (get (cl-read "#()") "elements") (list))
(cl-test "vector: mixed" (get (cl-read "#(a 1 \"s\")") "elements") (list "A" 1 {:cl-type "string" :value "s"}))

;; ── cl-read-all ───────────────────────────────────────────────────

(cl-test
  "read-all: empty"
  (cl-read-all "")
  (list))

(cl-test
  "read-all: two forms"
  (cl-read-all "42 foo")
  (list 42 "FOO"))

(cl-test
  "read-all: three forms"
  (cl-read-all "(+ 1 2) (+ 3 4) hello")
  (list (list "+" 1 2) (list "+" 3 4) "HELLO"))

(cl-test
  "read-all: with comments"
  (cl-read-all "; this is a comment\n42 ; inline\nfoo")
  (list 42 "FOO"))

(cl-test
  "read-all: defun form"
  (nth (cl-read-all "(defun square (x) (* x x))") 0)
  (list "DEFUN" "SQUARE" (list "X") (list "*" "X" "X")))
