;; ==========================================================================
;; test-collections.sx — Edge cases and complex patterns for collection ops
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: core.collections, core.dict, higher-order forms,
;;                 core.strings (string/collection bridge).
;; ==========================================================================


;; --------------------------------------------------------------------------
;; List operations — advanced edge cases
;; --------------------------------------------------------------------------

(defsuite "list-operations-advanced"
  (deftest "first of nested list returns inner list"
    (let ((nested (list (list 1 2) (list 3 4))))
      (assert-equal (list 1 2) (first nested))))

  (deftest "nested list is a list type"
    (let ((nested (list (list 1 2) (list 3 4))))
      (assert-type "list" (first nested))))

  (deftest "nth on nested list returns inner list"
    (let ((nested (list (list 1 2) (list 3 4))))
      (assert-equal (list 3 4) (nth nested 1))))

  (deftest "nth out of bounds returns nil"
    (assert-nil (nth (list 1 2 3) 10)))

  (deftest "nth negative index returns nil"
    ;; Negative indices are out-of-bounds — no wrap-around
    (let ((result (nth (list 1 2 3) -1)))
      (assert-true (or (nil? result) (number? result)))))

  (deftest "cons onto nil — platform-defined"
    ;; JS: cons 1 nil → [1, nil] (length 2)
    ;; Python: cons 1 nil → [1] (nil treated as empty list)
    ;; Both: first element is 1
    (assert-equal 1 (first (cons 1 nil))))

  (deftest "cons onto empty list produces single-element list"
    (assert-equal (list 1) (cons 1 (list)))
    (assert-equal 1 (len (cons 1 (list)))))

  (deftest "append with nil on right"
    ;; append(list, nil) — nil treated as empty or appended as element
    ;; The result is at least a list and starts with the original elements
    (let ((result (append (list 1 2) nil)))
      (assert-true (list? result))
      (assert-true (>= (len result) 2))
      (assert-equal 1 (first result))))

  (deftest "append two lists concatenates"
    (assert-equal (list 1 2 3 4)
                  (append (list 1 2) (list 3 4))))

  (deftest "concat three lists"
    (assert-equal (list 1 2 3) (concat (list 1) (list 2) (list 3))))

  (deftest "concat preserves order"
    (assert-equal (list "a" "b" "c" "d")
                  (concat (list "a" "b") (list "c" "d"))))

  (deftest "flatten one level of deeply nested"
    ;; flatten is one-level: ((( 1) 2) 3) → ((1) 2 3)
    (let ((deep (list (list (list 1) 2) 3))
          (result (flatten (list (list (list 1) 2) 3))))
      (assert-type "list" result)
      ;; 3 should now be a top-level element
      (assert-true (contains? result 3))))

  (deftest "flatten deeply nested — two passes"
    ;; Two flatten calls flatten two levels
    (let ((result (flatten (flatten (list (list (list 1 2) 3) 4)))))
      (assert-equal (list 1 2 3 4) result)))

  (deftest "flatten already-flat list is identity"
    (assert-equal (list 1 2 3) (flatten (list (list 1 2 3)))))

  (deftest "reverse single element"
    (assert-equal (list 42) (reverse (list 42))))

  (deftest "reverse preserves elements"
    (let ((original (list 1 2 3 4 5)))
      (let ((rev (reverse original)))
        (assert-equal 5 (len rev))
        (assert-equal 1 (last rev))
        (assert-equal 5 (first rev)))))

  (deftest "slice with start > end returns empty"
    ;; Slice where start exceeds end — implementation may clamp or return empty
    (let ((result (slice (list 1 2 3) 3 1)))
      (assert-true (or (nil? result)
                       (and (list? result) (empty? result))))))

  (deftest "slice with start at length returns empty"
    (let ((result (slice (list 1 2 3) 3)))
      (assert-true (or (nil? result)
                       (and (list? result) (empty? result))))))

  (deftest "range with step larger than range"
    ;; (range 0 3 10) — step exceeds range, should yield just (0)
    (let ((result (range 0 3 10)))
      (assert-equal (list 0) result)))

  (deftest "range step=1 is same as no step"
    (assert-equal (range 0 5) (range 0 5 1)))

  (deftest "map preserves order"
    (let ((result (map (fn (x) (* x 10)) (list 1 2 3 4 5))))
      (assert-equal 10 (nth result 0))
      (assert-equal 20 (nth result 1))
      (assert-equal 30 (nth result 2))
      (assert-equal 40 (nth result 3))
      (assert-equal 50 (nth result 4))))

  (deftest "filter preserves relative order"
    (let ((result (filter (fn (x) (> x 2)) (list 5 1 4 2 3))))
      (assert-equal 5 (nth result 0))
      (assert-equal 4 (nth result 1))
      (assert-equal 3 (nth result 2))))

  (deftest "reduce string concat left-to-right order"
    ;; (reduce f "" (list "a" "b" "c")) must be "abc" not "cba"
    (assert-equal "abc"
      (reduce (fn (acc x) (str acc x)) "" (list "a" "b" "c"))))

  (deftest "reduce subtraction is left-associative"
    ;; ((10 - 3) - 2) = 5, not (10 - (3 - 2)) = 9
    (assert-equal 5
      (reduce (fn (acc x) (- acc x)) 10 (list 3 2))))

  (deftest "map on empty list returns empty list"
    (assert-equal (list) (map (fn (x) (* x 2)) (list))))

  (deftest "filter on empty list returns empty list"
    (assert-equal (list) (filter (fn (x) true) (list)))))


;; --------------------------------------------------------------------------
;; Dict operations — advanced edge cases
;; --------------------------------------------------------------------------

(defsuite "dict-operations-advanced"
  (deftest "nested dict access via chained get"
    (let ((outer (dict "a" (dict "b" 42))))
      (assert-equal 42 (get (get outer "a") "b"))))

  (deftest "nested dict access — inner missing key returns nil"
    (let ((outer (dict "a" (dict "b" 42))))
      (assert-nil (get (get outer "a") "z"))))

  (deftest "assoc creates a new dict — original unchanged"
    (let ((original (dict "x" 1))
          (updated  (assoc (dict "x" 1) "y" 2)))
      (assert-false (has-key? original "y"))
      (assert-true  (has-key? updated  "y"))))

  (deftest "assoc preserves existing keys"
    (let ((d (dict "a" 1 "b" 2))
          (d2 (assoc (dict "a" 1 "b" 2) "c" 3)))
      (assert-equal 1 (get d2 "a"))
      (assert-equal 2 (get d2 "b"))
      (assert-equal 3 (get d2 "c"))))

  (deftest "assoc overwrites existing key"
    (let ((d (assoc (dict "a" 1) "a" 99)))
      (assert-equal 99 (get d "a"))))

  (deftest "dissoc creates a new dict — original unchanged"
    (let ((original (dict "a" 1 "b" 2))
          (reduced  (dissoc (dict "a" 1 "b" 2) "a")))
      (assert-true  (has-key? original "a"))
      (assert-false (has-key? reduced  "a"))))

  (deftest "dissoc missing key leaves dict unchanged"
    (let ((d (dict "a" 1 "b" 2))
          (d2 (dissoc (dict "a" 1 "b" 2) "z")))
      (assert-equal 2 (len d2))
      (assert-true (has-key? d2 "a"))
      (assert-true (has-key? d2 "b"))))

  (deftest "merge two dicts combines keys"
    (let ((d1 (dict "a" 1 "b" 2))
          (d2 (dict "c" 3 "d" 4))
          (merged (merge (dict "a" 1 "b" 2) (dict "c" 3 "d" 4))))
      (assert-equal 1 (get merged "a"))
      (assert-equal 2 (get merged "b"))
      (assert-equal 3 (get merged "c"))
      (assert-equal 4 (get merged "d"))))

  (deftest "merge — overlapping keys: second dict wins"
    (let ((merged (merge (dict "a" 1 "b" 2) (dict "b" 99 "c" 3))))
      (assert-equal 1  (get merged "a"))
      (assert-equal 99 (get merged "b"))
      (assert-equal 3  (get merged "c"))))

  (deftest "merge three dicts — rightmost wins on conflict"
    (let ((merged (merge (dict "k" 1) (dict "k" 2) (dict "k" 3))))
      (assert-equal 3 (get merged "k"))))

  (deftest "keys returns all keys"
    (let ((d (dict "x" 10 "y" 20 "z" 30)))
      (let ((ks (keys d)))
        (assert-equal 3 (len ks))
        (assert-true (contains? ks "x"))
        (assert-true (contains? ks "y"))
        (assert-true (contains? ks "z")))))

  (deftest "vals returns all values"
    (let ((d (dict "a" 1 "b" 2 "c" 3)))
      (let ((vs (vals d)))
        (assert-equal 3 (len vs))
        (assert-true (contains? vs 1))
        (assert-true (contains? vs 2))
        (assert-true (contains? vs 3)))))

  (deftest "len of nested dict counts top-level keys only"
    (let ((d (dict "a" (dict "x" 1 "y" 2) "b" 3)))
      (assert-equal 2 (len d))))

  (deftest "dict with numeric string keys"
    (let ((d (dict "1" "one" "2" "two")))
      (assert-equal "one" (get d "1"))
      (assert-equal "two" (get d "2"))))

  (deftest "dict with empty string key"
    (let ((d (dict "" "empty-key-value")))
      (assert-true (has-key? d ""))
      (assert-equal "empty-key-value" (get d ""))))

  (deftest "get with default on missing key"
    (let ((d (dict "a" 1)))
      (assert-equal 42 (get d "missing" 42))))

  (deftest "get on empty dict with default"
    (assert-equal "default" (get (dict) "any" "default"))))


;; --------------------------------------------------------------------------
;; List and dict interop
;; --------------------------------------------------------------------------

(defsuite "list-dict-interop"
  (deftest "map over list of dicts extracts field"
    (let ((items (list (dict "name" "Alice" "age" 30)
                       (dict "name" "Bob"   "age" 25)
                       (dict "name" "Carol" "age" 35))))
      (assert-equal (list "Alice" "Bob" "Carol")
                    (map (fn (d) (get d "name")) items))))

  (deftest "filter list of dicts by field value"
    (let ((items (list (dict "name" "Alice" "score" 80)
                       (dict "name" "Bob"   "score" 55)
                       (dict "name" "Carol" "score" 90)))
          (passing (filter (fn (d) (>= (get d "score") 70))
                           (list (dict "name" "Alice" "score" 80)
                                 (dict "name" "Bob"   "score" 55)
                                 (dict "name" "Carol" "score" 90)))))
      (assert-equal 2 (len passing))
      (assert-equal "Alice" (get (first passing) "name"))))

  (deftest "dict with list values"
    (let ((d (dict "tags" (list "a" "b" "c"))))
      (assert-true (list? (get d "tags")))
      (assert-equal 3 (len (get d "tags")))
      (assert-equal "b" (nth (get d "tags") 1))))

  (deftest "nested: dict containing list containing dict"
    (let ((data (dict "items" (list (dict "id" 1) (dict "id" 2)))))
      (let ((items (get data "items")))
        (assert-equal 2 (len items))
        (assert-equal 1 (get (first items) "id"))
        (assert-equal 2 (get (nth items 1) "id")))))

  (deftest "building a dict from a list via reduce"
    (let ((pairs (list (list "a" 1) (list "b" 2) (list "c" 3)))
          (result (reduce
                    (fn (acc pair)
                      (assoc acc (first pair) (nth pair 1)))
                    (dict)
                    (list (list "a" 1) (list "b" 2) (list "c" 3)))))
      (assert-equal 1 (get result "a"))
      (assert-equal 2 (get result "b"))
      (assert-equal 3 (get result "c"))))

  (deftest "keys then map to produce transformed dict"
    (let ((d (dict "a" 1 "b" 2 "c" 3))
          (ks (keys (dict "a" 1 "b" 2 "c" 3))))
      (let ((doubled (reduce
                       (fn (acc k) (assoc acc k (* (get d k) 2)))
                       (dict)
                       ks)))
        (assert-equal 2 (get doubled "a"))
        (assert-equal 4 (get doubled "b"))
        (assert-equal 6 (get doubled "c")))))

  (deftest "list of dicts — reduce to sum a field"
    (let ((records (list (dict "val" 10) (dict "val" 20) (dict "val" 30))))
      (assert-equal 60
        (reduce (fn (acc d) (+ acc (get d "val"))) 0 records))))

  (deftest "map-indexed with list of dicts attaches index"
    (let ((items (list (dict "name" "x") (dict "name" "y")))
          (result (map-indexed
                    (fn (i d) (assoc d "index" i))
                    (list (dict "name" "x") (dict "name" "y")))))
      (assert-equal 0 (get (first result) "index"))
      (assert-equal 1 (get (nth result 1) "index")))))


;; --------------------------------------------------------------------------
;; Collection equality
;; --------------------------------------------------------------------------

(defsuite "collection-equality"
  (deftest "two identical lists are equal"
    (assert-true (equal? (list 1 2 3) (list 1 2 3))))

  (deftest "= on same list reference is true"
    ;; = on the same reference is always true
    (let ((x (list 1 2)))
      (assert-true (= x x))))

  (deftest "different lists are not equal"
    (assert-false (equal? (list 1 2 3) (list 1 2 4))))

  (deftest "nested list equality"
    (assert-true (equal? (list 1 (list 2 3) 4)
                          (list 1 (list 2 3) 4))))

  (deftest "nested list inequality — inner differs"
    (assert-false (equal? (list 1 (list 2 3) 4)
                           (list 1 (list 2 99) 4))))

  (deftest "two identical dicts are equal"
    (assert-true (equal? (dict "a" 1 "b" 2)
                          (dict "a" 1 "b" 2))))

  (deftest "dicts with same keys/values but different insertion order are equal"
    ;; Dict equality is key/value structural, not insertion-order
    (let ((d1 (dict "a" 1 "b" 2))
          (d2 (assoc (dict "b" 2) "a" 1)))
      (assert-true (equal? d1 d2))))

  (deftest "empty list is not equal to nil"
    (assert-false (equal? (list) nil)))

  (deftest "empty list equals empty list"
    (assert-true (equal? (list) (list))))

  (deftest "order matters for list equality"
    (assert-false (equal? (list 1 2) (list 2 1))))

  (deftest "lists of different lengths are not equal"
    (assert-false (equal? (list 1 2) (list 1 2 3))))

  (deftest "empty dict equals empty dict"
    (assert-true (equal? (dict) (dict))))

  (deftest "dict with extra key is not equal"
    (assert-false (equal? (dict "a" 1) (dict "a" 1 "b" 2))))

  (deftest "list containing dict equality"
    (assert-true (equal? (list (dict "k" 1)) (list (dict "k" 1)))))

  (deftest "list containing dict inequality"
    (assert-false (equal? (list (dict "k" 1)) (list (dict "k" 2))))))


;; --------------------------------------------------------------------------
;; String / collection bridge
;; --------------------------------------------------------------------------

(defsuite "string-collection-bridge"
  (deftest "split then join round-trip"
    ;; Splitting on a separator then joining with the same separator recovers original
    (let ((original "a,b,c"))
      (assert-equal original (join "," (split original ",")))))

  (deftest "join then split round-trip"
    (let ((original (list "x" "y" "z")))
      (assert-equal original (split (join "-" original) "-"))))

  (deftest "split produces correct length"
    (assert-equal 3 (len (split "one:two:three" ":"))))

  (deftest "split produces list of strings"
    (let ((parts (split "a,b,c" ",")))
      (assert-true (every? string? parts))))

  (deftest "map over split result"
    ;; Split a CSV of numbers, parse each, sum
    (let ((nums (map parse-int (split "10,20,30" ","))))
      (assert-equal 60 (reduce (fn (a b) (+ a b)) 0 nums))))

  (deftest "join with empty separator concatenates"
    (assert-equal "abc" (join "" (list "a" "b" "c"))))

  (deftest "join single-element list returns the element"
    (assert-equal "hello" (join "," (list "hello"))))

  (deftest "split on non-present separator returns whole string in list"
    (let ((result (split "hello" ",")))
      (assert-equal 1 (len result))
      (assert-equal "hello" (first result))))

  (deftest "str on a list produces non-empty string"
    ;; Platform-defined formatting — just verify it's a non-empty string
    (let ((result (str (list 1 2 3))))
      (assert-true (string? result))
      (assert-true (not (empty? result)))))

  (deftest "upper then split preserves length"
    (let ((words (split "hello world foo" " ")))
      (let ((up-words (map upper words)))
        (assert-equal 3 (len up-words))
        (assert-equal "HELLO" (first up-words))
        (assert-equal "WORLD" (nth up-words 1))
        (assert-equal "FOO"   (nth up-words 2)))))

  (deftest "reduce over split to build string"
    ;; Re-join with a different separator
    (let ((words (split "a b c" " ")))
      (assert-equal "a|b|c" (join "|" words))))

  (deftest "split empty string on space"
    ;; Empty string split on space — platform may return list of one empty string or empty list
    (let ((result (split "" " ")))
      (assert-true (list? result))))

  (deftest "contains? works on joined string"
    (let ((sentence (join " " (list "the" "quick" "brown" "fox"))))
      (assert-true (contains? sentence "quick"))
      (assert-false (contains? sentence "lazy")))))
