;; Step 14: Source maps / error reporting
;; Source locations tracked through parse → compile → eval.
;; pos-to-loc converts character offset to {:line :col}.
;; Errors include source context.

;; ── pos-to-loc: offset → line/col ────────────────────────────────

(defsuite
  "pos-to-loc"
  (deftest
    "first char is line 1 col 0"
    (let
      ((loc (pos-to-loc "hello" 0)))
      (assert= (get loc :line) 1)
      (assert= (get loc :col) 0)))
  (deftest
    "mid-line position"
    (let
      ((loc (pos-to-loc "hello" 3)))
      (assert= (get loc :line) 1)
      (assert= (get loc :col) 3)))
  (deftest
    "second line"
    (let
      ((loc (pos-to-loc "abc\ndef" 4)))
      (assert= (get loc :line) 2)
      (assert= (get loc :col) 0)))
  (deftest
    "second line mid"
    (let
      ((loc (pos-to-loc "abc\ndef" 6)))
      (assert= (get loc :line) 2)
      (assert= (get loc :col) 2)))
  (deftest
    "third line"
    (let
      ((loc (pos-to-loc "a\nb\nc" 4)))
      (assert= (get loc :line) 3)
      (assert= (get loc :col) 0)))
  (deftest
    "end of input"
    (let
      ((loc (pos-to-loc "abc" 3)))
      (assert= (get loc :line) 1)
      (assert= (get loc :col) 3))))

;; ── Parser combinator error locations ─────────────────────────────

(defsuite
  "parse-error-loc"
  (deftest
    "failed parse includes line and col"
    (let
      ((result (run-parser digit "abc")))
      (assert (not (ok? result)))
      (let
        ((loc (error-loc result "abc")))
        (assert= (get loc :line) 1)
        (assert= (get loc :col) 0))))
  (deftest
    "failed parse after consuming"
    (let
      ((result (run-parser (seq2 letter digit) "ab")))
      (assert (not (ok? result)))
      (let
        ((loc (error-loc result "ab")))
        (assert= (get loc :line) 1)
        (assert= (get loc :col) 1))))
  (deftest
    "failed parse on second line"
    (let
      ((result (run-parser (seq2 (parse-char "\n") digit) "\na")))
      (assert (not (ok? result)))
      (let
        ((loc (error-loc result "\na")))
        (assert= (get loc :line) 2)
        (assert= (get loc :col) 0)))))

;; ── format-parse-error: human-readable error ──────────────────────

(defsuite
  "format-parse-error"
  (deftest
    "single line error"
    (let
      ((result (run-parser digit "hello"))
        (msg (format-parse-error result "hello")))
      (assert (string-contains? msg "line 1"))
      (assert (string-contains? msg "digit"))))
  (deftest
    "multi-line error shows line number"
    (let
      ((input "abc\ndef\n!!!")
        (result
          (run-parser (seq (list (parse-string "abc\ndef\n") letter)) input))
        (msg (format-parse-error result input)))
      (assert (string-contains? msg "line 3"))))
  (deftest
    "error includes source context"
    (let
      ((result (run-parser digit "let x = foo"))
        (msg (format-parse-error result "let x = foo")))
      (assert (string-contains? msg "let x = foo")))))

;; ── source-loc on parsed AST ──────────────────────────────────────

(defsuite
  "ast-source-loc"
  (deftest
    "sx-parse-loc returns forms with locations"
    (let
      ((forms (sx-parse-loc "(+ 1 2)")))
      (assert (not (empty? forms)))
      (let
        ((loc (source-loc (first forms))))
        (assert (not (nil? loc)))
        (assert= (get loc :line) 1)
        (assert= (get loc :col) 0))))
  (deftest
    "multi-line sx-parse-loc"
    (let
      ((forms (sx-parse-loc "(define x 1)\n(+ x 2)")))
      (assert= (len forms) 2)
      (let
        ((loc1 (source-loc (first forms)))
          (loc2 (source-loc (nth forms 1))))
        (assert= (get loc1 :line) 1)
        (assert= (get loc1 :col) 0)
        (assert= (get loc2 :line) 2)
        (assert= (get loc2 :col) 0))))
  (deftest
    "form value is accessible"
    (let
      ((forms (sx-parse-loc "(+ 1 2)")))
      (let
        ((form (get (first forms) :form)))
        (assert (list? form))
        (assert= (len form) 3)))))
