;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL

(ns common-tests.data-test
  (:require
   [app.common.data :as d]
   [clojure.test :as t]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Basic Predicates
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest boolean-or-nil-predicate
  (t/is (d/boolean-or-nil? nil))
  (t/is (d/boolean-or-nil? true))
  (t/is (d/boolean-or-nil? false))
  (t/is (not (d/boolean-or-nil? 0)))
  (t/is (not (d/boolean-or-nil? "")))
  (t/is (not (d/boolean-or-nil? :kw))))

(t/deftest in-range-predicate
  (t/is (d/in-range? 5 0))
  (t/is (d/in-range? 5 4))
  (t/is (not (d/in-range? 5 5)))
  (t/is (not (d/in-range? 5 -1)))
  (t/is (not (d/in-range? 0 0))))

(t/deftest get-initials-test
  (t/is (= "JD" (d/get-initials "John Doe")))
  (t/is (= "A" (d/get-initials "acme")))
  (t/is (= "AB" (d/get-initials "123 Alpha ## beta")))
  (t/is (= "PD" (d/get-initials "  penpot   design  tool ")))
  (t/is (= "" (d/get-initials nil)))
  (t/is (= "" (d/get-initials "!!! ???"))))

(t/deftest normalize-string-test
  ;; nil input returns empty string
  (t/is (= "" (d/normalize-string nil)))
  ;; empty string returns empty string
  (t/is (= "" (d/normalize-string "")))
  ;; leading whitespace is trimmed
  (t/is (= "hello" (d/normalize-string "  hello")))
  ;; trailing whitespace is trimmed
  (t/is (= "hello" (d/normalize-string "hello  ")))
  ;; both leading and trailing whitespace are trimmed
  (t/is (= "hello" (d/normalize-string "  hello  ")))
  ;; internal whitespace is preserved
  (t/is (= "hello world" (d/normalize-string "  hello world  ")))
  ;; non-string input is returned unchanged
  (t/is (= 42 (d/normalize-string 42)))
  (t/is (= :keyword (d/normalize-string :keyword)))
  (t/is (= true (d/normalize-string true))))

(t/deftest escape-markdown-test
  (t/is (= "hello" (d/escape-markdown "hello")))
  (t/is (= "" (d/escape-markdown nil)))
  (t/is (= "" (d/escape-markdown "")))
  (t/is (= "\\*bold\\*" (d/escape-markdown "*bold*")))
  (t/is (= "\\_italic\\_" (d/escape-markdown "_italic_")))
  (t/is (= "\\~strikethrough\\~" (d/escape-markdown "~strikethrough~")))
  (t/is (= "\\`code\\`" (d/escape-markdown "`code`")))
  (t/is (= "\\[link\\]\\(http://evil\\.com\\)" (d/escape-markdown "[link](http://evil.com)")))
  (t/is (= "\\> quote" (d/escape-markdown "> quote")))
  (t/is (= "\\# heading" (d/escape-markdown "# heading")))
  (t/is (= "\\@channel" (d/escape-markdown "@channel")))
  (t/is (= "\\!bang" (d/escape-markdown "!bang")))
  (t/is (= "normal\\-text" (d/escape-markdown "normal-text")))
  (t/is (= "a\\+b\\=c" (d/escape-markdown "a+b=c")))
  (t/is (= "pipe\\|separated" (d/escape-markdown "pipe|separated")))
  (t/is (= "curly\\{\\}braces" (d/escape-markdown "curly{}braces")))
  (t/is (= "backslash\\\\slash" (d/escape-markdown "backslash\\slash"))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Ordered Data Structures
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest ordered-set-creation
  (let [s (d/ordered-set)]
    (t/is (d/ordered-set? s))
    (t/is (empty? s)))
  (let [s (d/ordered-set :a)]
    (t/is (d/ordered-set? s))
    (t/is (contains? s :a)))
  (let [s (d/ordered-set :a :b :c)]
    (t/is (d/ordered-set? s))
    (t/is (= (seq s) [:a :b :c]))))

(t/deftest ordered-set-preserves-order
  (let [s (d/ordered-set :c :a :b)]
    (t/is (= (seq s) [:c :a :b])))
  ;; Duplicates are ignored; order of first insertion is kept
  (let [s (-> (d/ordered-set) (conj :a) (conj :b) (conj :a))]
    (t/is (= (seq s) [:a :b]))))

(t/deftest ordered-map-creation
  (let [m (d/ordered-map)]
    (t/is (d/ordered-map? m))
    (t/is (empty? m)))
  (let [m (d/ordered-map :a 1)]
    (t/is (d/ordered-map? m))
    (t/is (= (get m :a) 1)))
  (let [m (d/ordered-map :a 1 :b 2)]
    (t/is (d/ordered-map? m))
    (t/is (= (keys m) [:a :b]))))

(t/deftest ordered-map-preserves-insertion-order
  (let [m (-> (d/ordered-map)
              (assoc :c 3)
              (assoc :a 1)
              (assoc :b 2))]
    (t/is (= (keys m) [:c :a :b]))))

(t/deftest oassoc-test
  ;; oassoc on nil creates a new ordered-map
  (let [m (d/oassoc nil :a 1 :b 2)]
    (t/is (d/ordered-map? m))
    (t/is (= (get m :a) 1))
    (t/is (= (get m :b) 2)))
  ;; oassoc on existing ordered-map updates it
  (let [m (d/oassoc (d/ordered-map :x 10) :y 20)]
    (t/is (= (get m :x) 10))
    (t/is (= (get m :y) 20))))

(t/deftest oassoc-in-test
  (let [m (d/oassoc-in nil [:a :b] 42)]
    (t/is (d/ordered-map? m))
    (t/is (= (get-in m [:a :b]) 42)))
  (let [m (-> (d/ordered-map)
              (d/oassoc-in [:x :y] 1)
              (d/oassoc-in [:x :z] 2))]
    (t/is (= (get-in m [:x :y]) 1))
    (t/is (= (get-in m [:x :z]) 2))))

(t/deftest oupdate-in-test
  (let [m (-> (d/ordered-map)
              (d/oassoc-in [:a :b] 10)
              (d/oupdate-in [:a :b] + 5))]
    (t/is (= (get-in m [:a :b]) 15))))

(t/deftest oassoc-before-test
  (let [m (-> (d/ordered-map)
              (assoc :a 1)
              (assoc :b 2)
              (assoc :c 3))
        m2 (d/oassoc-before m :b :x 99)]
    ;; :x should be inserted just before :b
    (t/is (= (keys m2) [:a :x :b :c]))
    (t/is (= (get m2 :x) 99)))
  ;; When before-k does not exist, assoc at the end
  (let [m  (-> (d/ordered-map) (assoc :a 1))
        m2 (d/oassoc-before m :z :x 99)]
    (t/is (= (get m2 :x) 99))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Ordered Set / Map Index Helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest adds-at-index-test
  (let [s (d/ordered-set :a :b :c)
        s2 (d/adds-at-index s 1 :x)]
    (t/is (= (seq s2) [:a :x :b :c])))
  (let [s  (d/ordered-set :a :b :c)
        s2 (d/adds-at-index s 0 :x)]
    (t/is (= (seq s2) [:x :a :b :c])))
  (let [s  (d/ordered-set :a :b :c)
        s2 (d/adds-at-index s 3 :x)]
    (t/is (= (seq s2) [:a :b :c :x]))))

(t/deftest inserts-at-index-test
  (let [s  (d/ordered-set :a :b :c)
        s2 (d/inserts-at-index s 1 [:x :y])]
    (t/is (= (seq s2) [:a :x :y :b :c])))
  (let [s  (d/ordered-set :a :b :c)
        s2 (d/inserts-at-index s 0 [:x])]
    (t/is (= (seq s2) [:x :a :b :c]))))

(t/deftest addm-at-index-test
  (let [m  (-> (d/ordered-map) (assoc :a 1) (assoc :b 2) (assoc :c 3))
        m2 (d/addm-at-index m 1 :x 99)]
    (t/is (= (keys m2) [:a :x :b :c]))
    (t/is (= (get m2 :x) 99))))

(t/deftest insertm-at-index-test
  (let [m  (-> (d/ordered-map) (assoc :a 1) (assoc :b 2) (assoc :c 3))
        m2 (d/insertm-at-index m 1 (d/ordered-map :x 10 :y 20))]
    (t/is (= (keys m2) [:a :x :y :b :c]))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Concat / remove helpers (pre-existing tests preserved)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest concat-vec
  (t/is (= []    (d/concat-vec)))
  (t/is (= [1]   (d/concat-vec [1])))
  (t/is (= [1]   (d/concat-vec #{1})))
  (t/is (= [1 2] (d/concat-vec [1] #{2})))
  (t/is (= [1 2] (d/concat-vec '(1) [2]))))

(t/deftest concat-set
  (t/is (= #{} (d/concat-set)))
  (t/is (= #{1 2}
           (d/concat-set [1] [2]))))

(t/deftest remove-at-index
  (t/is (= [1 2 3 4]
           (d/remove-at-index [1 2 3 4 5] 4)))


  (t/is (= [1 2 3 4]
           (d/remove-at-index [5 1 2 3 4] 0)))

  (t/is (= [1 2 3 4]
           (d/remove-at-index [1 5 2 3 4] 1))))

(t/deftest with-next
  (t/is (= [[0 1] [1 2] [2 3] [3 4] [4 nil]]
           (d/with-next (range 5)))))

(t/deftest with-prev
  (t/is (= [[0 nil] [1 0] [2 1] [3 2] [4 3]]
           (d/with-prev (range 5)))))

(t/deftest with-prev-next
  (t/is (= [[0 nil 1] [1 0 2] [2 1 3] [3 2 4] [4 3 nil]]
           (d/with-prev-next (range 5)))))

(t/deftest join
  (t/is (= [[1 :a] [1 :b] [2 :a] [2 :b] [3 :a] [3 :b]]
           (d/join [1 2 3] [:a :b])))
  (t/is (= [1 10 100 2 20 200 3 30 300]
           (d/join [1 2 3] [1 10 100] *))))

(t/deftest num-predicate
  (t/is (not (d/num? ##NaN)))
  (t/is (not (d/num? nil)))
  (t/is (d/num? 1))
  (t/is (d/num? -0.3))
  (t/is (not (d/num? {}))))

(t/deftest check-num-helper
  (t/is (= 1 (d/check-num 1 0)))
  (t/is (= 0 (d/check-num ##NaN 0)))
  (t/is (= 0 (d/check-num {} 0)))
  (t/is (= 0 (d/check-num [] 0)))
  (t/is (= 0 (d/check-num :foo 0)))
  (t/is (= 0 (d/check-num nil 0))))

(t/deftest insert-at-index
  ;; insert different object
  (t/is (= (d/insert-at-index [:a :b] 1 [:c :d])
           [:a :c :d :b]))

  ;; insert on the start
  (t/is (= (d/insert-at-index [:a :b] 0 [:c])
           [:c :a :b]))

  ;; insert on the end 1
  (t/is (= (d/insert-at-index [:a :b] 2 [:c])
           [:a :b :c]))

  ;; insert on the end with not existing index
  (t/is (= (d/insert-at-index [:a :b] 10 [:c])
           [:a :b :c]))

  ;; insert existing in a contiguous index
  (t/is (= (d/insert-at-index [:a :b] 1 [:a])
           [:a :b]))

  ;; insert existing in the same index
  (t/is (= (d/insert-at-index [:a :b] 0 [:a])
           [:a :b]))

  ;; insert existing in other index case 1
  (t/is (= (d/insert-at-index [:a :b :c] 2 [:a])
           [:b :a :c]))

  ;; insert existing in other index case 2
  (t/is (= (d/insert-at-index [:a :b :c :d] 0 [:d])
           [:d :a :b :c]))

  ;; insert existing in other index case 3
  (t/is (= (d/insert-at-index [:a :b :c :d] 1 [:a])
           [:a :b :c :d])))

(t/deftest reorder
  (let [v ["a" "b" "c" "d"]]
    (t/is (= (d/reorder v 0 2) ["b" "a" "c" "d"]))
    (t/is (= (d/reorder v 0 3) ["b" "c" "a" "d"]))
    (t/is (= (d/reorder v 0 4) ["b" "c" "d" "a"]))
    (t/is (= (d/reorder v 3 0) ["d" "a" "b" "c"]))
    (t/is (= (d/reorder v 3 2) ["a" "b" "d" "c"]))
    (t/is (= (d/reorder v 0 5) ["b" "c" "d" "a"]))
    (t/is (= (d/reorder v 3 -1) ["d" "a" "b" "c"]))
    (t/is (= (d/reorder v 5 -1) ["d" "a" "b" "c"]))
    (t/is (= (d/reorder v -1 5) ["b" "c" "d" "a"]))))

(t/deftest nth-last-index-of-test
  (t/is (= (d/nth-last-index-of "" "*" 1) nil))
  (t/is (= (d/nth-last-index-of "*abc" "*" 1) 0))
  (t/is (= (d/nth-last-index-of "**abc" "*" 2) 0))
  (t/is (= (d/nth-last-index-of "abc*def*ghi" "*" 3) nil))
  (t/is (= (d/nth-last-index-of "" "*" 2) nil))
  (t/is (= (d/nth-last-index-of "abc*" "*" 1) 3))
  (t/is (= (d/nth-last-index-of "abc*" "*" 2) nil))
  (t/is (= (d/nth-last-index-of "*abc[*" "*" 1) 5))
  (t/is (= (d/nth-last-index-of "abc*def*ghi" "*" 1) 7))
  (t/is (= (d/nth-last-index-of "abc*def*ghi" "*" 2) 3)))

(t/deftest nth-index-of-test
  (t/is (= (d/nth-index-of "" "*" 1) nil))
  (t/is (= (d/nth-index-of "" "*" 2) nil))
  (t/is (= (d/nth-index-of "abc*" "*" 1) 3))
  (t/is (= (d/nth-index-of "abc*" "*" 1) 3))
  (t/is (= (d/nth-index-of "abc**" "*" 2) 4))
  (t/is (= (d/nth-index-of "abc*" "*" 2) nil))
  (t/is (= (d/nth-index-of "*abc[*" "*" 1) 0))
  (t/is (= (d/nth-index-of "abc*def*ghi" "*" 1) 3))
  (t/is (= (d/nth-index-of "abc*def*ghi" "*" 2) 7))
  (t/is (= (d/nth-index-of "abc*def*ghi" "*" 3) nil)))

(t/deftest natural-sort-by-test
  (t/is (= (d/natural-sort-by identity ["10" "2" "1" "11" "3" "30"])
           ["1" "2" "3" "10" "11" "30"]))
  (t/is (= (d/natural-sort-by identity ["banana" "apple" "cherry"])
           ["apple" "banana" "cherry"]))
  (t/is (= (d/natural-sort-by identity ["size10" "size2" "size1" "size20" "size3"])
           ["size1" "size2" "size3" "size10" "size20"]))
  (t/is (= (d/natural-sort-by identity ["b1" "a2" "a10" "a1"])
           ["a1" "a2" "a10" "b1"]))
  (t/is (= (d/natural-sort-by identity []) []))
  (t/is (= (d/natural-sort-by identity ["solo"]) ["solo"]))
  (t/is (= (d/natural-sort-by identity ["b" "a" "a" "c"])
           ["a" "a" "b" "c"]))
  (t/is (= (d/natural-sort-by :name
                              [{:name "big"} {:name "small"} {:name "medium"}])
           [{:name "big"} {:name "medium"} {:name "small"}]))
  (t/is (= (d/natural-sort-by :name
                              [{:name "size10"} {:name "size2"} {:name "size1"}])
           [{:name "size1"} {:name "size2"} {:name "size10"}]))
  (t/is (= (d/natural-sort-by :name
                              [{:name "border-radius-10"}
                               {:name "border-radius-2"}
                               {:name "border-radius-1"}])
           [{:name "border-radius-1"}
            {:name "border-radius-2"}
            {:name "border-radius-10"}]))
  (t/is (= (d/natural-sort-by :name
                              [{:name "border-10-radius"}
                               {:name "border-2-radius"}
                               {:name "border-1-radius"}])
           [{:name "border-1-radius"}
            {:name "border-2-radius"}
            {:name "border-10-radius"}]))
  (t/is (= (d/natural-sort-by :name
                              [{:name "border-10-radius"}
                               {:name "border-2-extra"}
                               {:name "border-2-radius"}
                               {:name "border-1-radius"}])
           [{:name "border-1-radius"}
            {:name "border-2-extra"}
            {:name "border-2-radius"}
            {:name "border-10-radius"}])))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Lazy / sequence helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest concat-all-test
  (t/is (= [1 2 3 4 5 6]
           (d/concat-all [[1 2] [3 4] [5 6]])))
  (t/is (= [] (d/concat-all [])))
  (t/is (= [1 2 3]
           (d/concat-all [[1] [2] [3]])))
  ;; It's lazy — works with infinite-ish inner seqs truncated by outer limit
  (t/is (= [0 1 2]
           (take 3 (d/concat-all (map list (range)))))))

(t/deftest mapcat-test
  (t/is (= [0 1 1 2 2 3]
           (d/mapcat (fn [x] [x (inc x)]) [0 1 2])))
  ;; fully lazy — can operate on infinite sequences
  (t/is (= [0 0 1 1 2 2]
           (take 6 (d/mapcat (fn [x] [x x]) (range))))))

(t/deftest zip-test
  (t/is (= [[1 :a] [2 :b] [3 :c]]
           (d/zip [1 2 3] [:a :b :c])))
  (t/is (= [] (d/zip [] []))))

(t/deftest zip-all-test
  ;; same length
  (t/is (= [[1 :a] [2 :b]]
           (d/zip-all [1 2] [:a :b])))
  ;; col1 longer — col2 padded with nils
  (t/is (= [[1 :a] [2 nil] [3 nil]]
           (d/zip-all [1 2 3] [:a])))
  ;; col2 longer — col1 padded with nils
  (t/is (= [[1 :a] [nil :b] [nil :c]]
           (d/zip-all [1] [:a :b :c]))))

(t/deftest enumerate-test
  (t/is (= [[0 :a] [1 :b] [2 :c]]
           (d/enumerate [:a :b :c])))
  (t/is (= [[5 :a] [6 :b]]
           (d/enumerate [:a :b] 5)))
  (t/is (= [] (d/enumerate []))))

(t/deftest interleave-all-test
  (t/is (= [] (d/interleave-all)))
  (t/is (= [1 2 3] (d/interleave-all [1 2 3])))
  (t/is (= [1 :a 2 :b 3 :c]
           (d/interleave-all [1 2 3] [:a :b :c])))
  ;; unequal lengths — longer seq is not truncated
  (t/is (= [1 :a 2 :b 3]
           (d/interleave-all [1 2 3] [:a :b])))
  (t/is (= [1 :a 2 :b :c]
           (d/interleave-all [1 2] [:a :b :c]))))

(t/deftest add-at-index-test
  (t/is (= [:a :x :b :c] (d/add-at-index [:a :b :c] 1 :x)))
  (t/is (= [:x :a :b :c] (d/add-at-index [:a :b :c] 0 :x)))
  (t/is (= [:a :b :c :x] (d/add-at-index [:a :b :c] 3 :x))))

(t/deftest take-until-test
  ;; stops (inclusive) when predicate is true
  (t/is (= [1 2 3] (d/take-until #(= % 3) [1 2 3 4 5])))
  ;; if predicate never true, returns whole collection
  (t/is (= [1 2 3] (d/take-until #(= % 9) [1 2 3])))
  ;; first element matches
  (t/is (= [1] (d/take-until #(= % 1) [1 2 3]))))

(t/deftest safe-subvec-test
  ;; normal range
  (t/is (= [2 3] (d/safe-subvec [1 2 3 4] 1 3)))
  ;; single arg — from index to end
  (t/is (= [2 3 4] (d/safe-subvec [1 2 3 4] 1)))
  ;; start=0 returns the full vector
  (t/is (= [1 2 3 4] (d/safe-subvec [1 2 3 4] 0)))
  ;; out-of-range returns nil
  (t/is (nil? (d/safe-subvec [1 2 3] 5)))
  (t/is (nil? (d/safe-subvec [1 2 3] 0 5)))
  ;; nil v returns nil
  (t/is (nil? (d/safe-subvec nil 0 1))))

(t/deftest domap-test
  (let [side-effects (atom [])
        result       (d/domap #(swap! side-effects conj %) [1 2 3])]
    (t/is (= [1 2 3] result))
    (t/is (= [1 2 3] @side-effects)))
  ;; transducer arity
  (let [side-effects (atom [])]
    (into [] (d/domap #(swap! side-effects conj %)) [4 5])
    (t/is (= [4 5] @side-effects))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Collection lookup helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest group-by-test
  (t/is (= {:odd [1 3] :even [2 4]}
           (d/group-by #(if (odd? %) :odd :even) [1 2 3 4])))
  ;; two-arity with value function
  (t/is (= {:odd [10 30] :even [20 40]}
           (d/group-by #(if (odd? %) :odd :even) #(* % 10) [1 2 3 4])))
  ;; three-arity with initial value
  (t/is (= {:a #{1} :b #{2}}
           (d/group-by :k :v #{} [{:k :a :v 1} {:k :b :v 2}]))))

(t/deftest seek-test
  (t/is (= 3 (d/seek odd? [2 4 3 5])))
  (t/is (nil? (d/seek odd? [2 4 6])))
  (t/is (= :default (d/seek odd? [2 4 6] :default)))
  (t/is (= 1 (d/seek some? [nil nil 1 2]))))

(t/deftest index-by-test
  (t/is (= {1 {:id 1 :name "a"} 2 {:id 2 :name "b"}}
           (d/index-by :id [{:id 1 :name "a"} {:id 2 :name "b"}])))
  ;; two-arity with value fn
  (t/is (= {1 "a" 2 "b"}
           (d/index-by :id :name [{:id 1 :name "a"} {:id 2 :name "b"}]))))

(t/deftest index-of-pred-test
  (t/is (= 0 (d/index-of-pred [1 2 3] odd?)))
  (t/is (= 1 (d/index-of-pred [2 3 4] odd?)))
  (t/is (nil? (d/index-of-pred [2 4 6] odd?)))
  (t/is (nil? (d/index-of-pred [] odd?)))
  ;; works correctly when collection contains nil elements
  (t/is (= 2 (d/index-of-pred [nil nil 3] some?)))
  (t/is (= 0 (d/index-of-pred [nil 1 2] nil?)))
  ;; works correctly when collection contains false elements
  (t/is (= 1 (d/index-of-pred [false true false] true?))))

(t/deftest index-of-test
  (t/is (= 0 (d/index-of [:a :b :c] :a)))
  (t/is (= 2 (d/index-of [:a :b :c] :c)))
  (t/is (nil? (d/index-of [:a :b :c] :z)))
  ;; works when searching for nil in a collection
  (t/is (= 1 (d/index-of [:a nil :c] nil))))

(t/deftest replace-by-id-test
  (let [items [{:id 1 :v "a"} {:id 2 :v "b"} {:id 3 :v "c"}]
        new-v {:id 2 :v "x"}]
    (t/is (= [{:id 1 :v "a"} {:id 2 :v "x"} {:id 3 :v "c"}]
             (d/replace-by-id items new-v)))
    ;; transducer arity
    (t/is (= [{:id 1 :v "a"} {:id 2 :v "x"} {:id 3 :v "c"}]
             (sequence (d/replace-by-id new-v) items)))))

(t/deftest getf-test
  (let [m {:a 1 :b 2}
        get-from-m (d/getf m)]
    (t/is (= 1 (get-from-m :a)))
    (t/is (= 2 (get-from-m :b)))
    (t/is (nil? (get-from-m :z)))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Map manipulation helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest vec-without-nils-test
  (t/is (= [1 2 3] (d/vec-without-nils [1 nil 2 nil 3])))
  (t/is (= [] (d/vec-without-nils [nil nil])))
  (t/is (= [1] (d/vec-without-nils [1]))))

(t/deftest without-nils-test
  (t/is (= {:a 1 :d 2} (d/without-nils {:a 1 :b nil :c nil :d 2 :e nil}))
        "removes all nil values")
  ;; transducer arity — works on map entries
  (t/is (= {:a 1} (into {} (d/without-nils) {:a 1 :b nil}))))

(t/deftest without-qualified-test
  (t/is (= {:a 1} (d/without-qualified {:a 1 :ns/b 2 :ns/c 3})))
  ;; transducer arity — works on map entries
  (t/is (= {:a 1} (into {} (d/without-qualified) {:a 1 :ns/b 2}))))

(t/deftest without-keys-test
  (t/is (= {:c 3} (d/without-keys {:a 1 :b 2 :c 3} [:a :b])))
  (t/is (= {:a 1 :b 2 :c 3} (d/without-keys {:a 1 :b 2 :c 3} []))))

(t/deftest deep-merge-test
  (t/is (= {:a 1 :b {:c 3 :d 4}}
           (d/deep-merge {:a 1 :b {:c 2 :d 4}} {:b {:c 3}})))
  ;; non-map values get replaced
  (t/is (= {:a 2}
           (d/deep-merge {:a 1} {:a 2})))
  ;; three-way merge
  (t/is (= {:a 1 :b 2 :c 3}
           (d/deep-merge {:a 1} {:b 2} {:c 3}))))

(t/deftest dissoc-in-test
  (t/is (= {:a {:b 1}} (d/dissoc-in {:a {:b 1 :c 2}} [:a :c])))
  ;; removes parent when child map becomes empty
  (t/is (= {} (d/dissoc-in {:a {:b 1}} [:a :b])))
  ;; no-op when path does not exist
  (t/is (= {:a 1} (d/dissoc-in {:a 1} [:b :c]))))

(t/deftest patch-object-test
  ;; normal update
  (t/is (= {:a 2 :b 2} (d/patch-object {:a 1 :b 2} {:a 2})))
  ;; nil value removes key
  (t/is (= {:b 2} (d/patch-object {:a 1 :b 2} {:a nil})))
  ;; nested map is merged recursively
  (t/is (= {:a {:x 10 :y 2}} (d/patch-object {:a {:x 1 :y 2}} {:a {:x 10}})))
  ;; nested nil removes nested key
  (t/is (= {:a {:y 2}} (d/patch-object {:a {:x 1 :y 2}} {:a {:x nil}})))
  ;; nil value removes only the specified key, not other keys
  (t/is (= {nil 0 :b 2} (d/patch-object {nil 0 :a 1 :b 2} {:a nil})))
  ;; transducer arity (1-arg returns a fn)
  (let [f (d/patch-object {:a 99})]
    (t/is (= {:a 99 :b 2} (f {:a 1 :b 2}))))
  ;; when object is nil, nil values in changes are stripped (not preserved)
  (t/is (= {} (d/patch-object nil {:a nil})))
  (t/is (= {:a 1} (d/patch-object nil {:a 1 :b nil})))
  ;; nested path: patching a key that doesn't exist creates a new map without nils
  (t/is (= {:b {:y 2}} (d/patch-object {:b nil} {:b {:x nil :y 2}}))))

(t/deftest without-obj-test
  (t/is (= [1 3] (d/without-obj [1 2 3] 2)))
  (t/is (= [1 2 3] (d/without-obj [1 2 3] 9)))
  (t/is (= [] (d/without-obj [1] 1))))

(t/deftest update-vals-test
  (t/is (= {:a 2 :b 4} (d/update-vals {:a 1 :b 2} #(* % 2))))
  (t/is (= {} (d/update-vals {} identity))))

(t/deftest update-in-when-test
  ;; key exists — applies function
  (t/is (= {:a {:b 2}} (d/update-in-when {:a {:b 1}} [:a :b] inc)))
  ;; key absent — returns unchanged
  (t/is (= {:a 1} (d/update-in-when {:a 1} [:b :c] inc))))

(t/deftest update-when-test
  ;; key exists — applies function
  (t/is (= {:a 2} (d/update-when {:a 1} :a inc)))
  ;; key absent — returns unchanged
  (t/is (= {:a 1} (d/update-when {:a 1} :b inc))))

(t/deftest assoc-in-when-test
  ;; key exists — updates value
  (t/is (= {:a {:b 99}} (d/assoc-in-when {:a {:b 1}} [:a :b] 99)))
  ;; key absent — returns unchanged
  (t/is (= {:a 1} (d/assoc-in-when {:a 1} [:b :c] 99))))

(t/deftest assoc-when-test
  ;; key exists — updates value
  (t/is (= {:a 99} (d/assoc-when {:a 1} :a 99)))
  ;; key absent — returns unchanged
  (t/is (= {:a 1} (d/assoc-when {:a 1} :b 99))))

(t/deftest merge-test
  (t/is (= {:a 1 :b 2 :c 3}
           (d/merge {:a 1} {:b 2} {:c 3})))
  (t/is (= {:a 2}
           (d/merge {:a 1} {:a 2})))
  (t/is (= {} (d/merge))))

(t/deftest txt-merge-test
  ;; sets value when not nil
  (t/is (= {:a 2 :b 2} (d/txt-merge {:a 1 :b 2} {:a 2})))
  ;; removes key when value is nil
  (t/is (= {:b 2} (d/txt-merge {:a 1 :b 2} {:a nil})))
  ;; adds new key
  (t/is (= {:a 1 :b 2 :c 3} (d/txt-merge {:a 1 :b 2} {:c 3}))))

(t/deftest mapm-test
  ;; two-arity: transform map in place
  (t/is (= {:a 2 :b 4} (d/mapm (fn [k v] (* v 2)) {:a 1 :b 2})))
  ;; one-arity: transducer
  (t/is (= {:a 10 :b 20}
           (into {} (d/mapm (fn [k v] (* v 10))) {:a 1 :b 2}))))

(t/deftest removev-test
  (t/is (= [2 4] (d/removev odd? [1 2 3 4])))
  (t/is (= [nil nil] (d/removev some? [nil nil])))
  (t/is (= [1 2 3] (d/removev nil? [1 nil 2 nil 3]))))

(t/deftest filterm-test
  (t/is (= {:a 1 :c 3} (d/filterm (fn [[_ v]] (odd? v)) {:a 1 :b 2 :c 3 :d 4}))
        "keeps entries where value is odd")
  (t/is (= {} (d/filterm (fn [[_ v]] (> v 10)) {:a 1 :b 2}))))

(t/deftest removem-test
  (t/is (= {:b 2 :d 4} (d/removem (fn [[_ v]] (odd? v)) {:a 1 :b 2 :c 3 :d 4})))
  (t/is (= {:a 1 :b 2} (d/removem (fn [[_ v]] (> v 10)) {:a 1 :b 2}))))

(t/deftest map-perm-test
  ;; default: all pairs
  (t/is (= [[1 2] [1 3] [1 4] [2 3] [2 4] [3 4]]
           (d/map-perm vector [1 2 3 4])))
  ;; with predicate
  (t/is (= [[1 3]]
           (d/map-perm vector (fn [a b] (and (odd? a) (odd? b))) [1 2 3])))
  ;; empty collection
  (t/is (= [] (d/map-perm vector []))))

(t/deftest distinct-xf-test
  (t/is (= [1 2 3]
           (into [] (d/distinct-xf identity) [1 2 1 3 2])))
  ;; keeps the first occurrence for each key
  (t/is (= [{:id 1 :v "a"} {:id 2 :v "x"}]
           (into [] (d/distinct-xf :id) [{:id 1 :v "a"} {:id 2 :v "x"} {:id 2 :v "b"}]))))

(t/deftest deep-mapm-test
  ;; mfn is applied once per entry
  (t/is (= {:a 2 :b {:c 4}}
           (d/deep-mapm (fn [[k v]] [k (if (number? v) (* v 2) v)])
                        {:a 1 :b {:c 2}})))
  ;; Keyword renaming: keys are transformed once per entry
  (let [result (d/deep-mapm (fn [[k v]] [(keyword (str (name k) "!")) v])
                            {:a 1})]
    (t/is (contains? result (keyword "a!"))))
  ;; Vectors inside maps are recursed into
  (t/is (= {:items [{:x 10}]}
           (d/deep-mapm (fn [[k v]] [k (if (number? v) (* v 10) v)])
                        {:items [{:x 1}]})))
  ;; Plain scalar at top level map
  (t/is (= {:a "hello"} (d/deep-mapm identity {:a "hello"}))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Numeric helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest nan-test
  (t/is (d/nan? ##NaN))
  (t/is (not (d/nan? 0)))
  (t/is (not (d/nan? 1)))
  (t/is (not (d/nan? nil)))
  ;; CLJS js/isNaN coerces non-numbers; JVM Double/isNaN is number-only
  #?(:cljs (t/is (d/nan? "hello")))
  #?(:clj  (t/is (not (d/nan? "hello")))))

(t/deftest safe-plus-test
  (t/is (= 5 (d/safe+ 3 2)))
  ;; when first arg is not finite, return it unchanged
  (t/is (= ##Inf (d/safe+ ##Inf 10))))

(t/deftest max-test
  (t/is (= 3 (d/max 3)))
  (t/is (= 5 (d/max 3 5)))
  (t/is (= 9 (d/max 1 9 4)))
  (t/is (= 10 (d/max 1 2 3 4 5 6 7 8 9 10))))

(t/deftest min-test
  (t/is (= 3 (d/min 3)))
  (t/is (= 3 (d/min 3 5)))
  (t/is (= 1 (d/min 1 9 4)))
  (t/is (= 1 (d/min 10 9 8 7 6 5 4 3 2 1))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Parsing helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest parse-integer-test
  (t/is (= 42 (d/parse-integer "42")))
  (t/is (= -1 (d/parse-integer "-1")))
  (t/is (nil? (d/parse-integer "abc")))
  (t/is (= 0 (d/parse-integer "abc" 0)))
  (t/is (nil? (d/parse-integer nil))))

(t/deftest parse-double-test
  (t/is (= 3.14 (d/parse-double "3.14")))
  (t/is (= -1.0 (d/parse-double "-1.0")))
  (t/is (nil? (d/parse-double "abc")))
  (t/is (= 0.0 (d/parse-double "abc" 0.0)))
  (t/is (nil? (d/parse-double nil))))

(t/deftest parse-uuid-test
  (let [uuid-str "550e8400-e29b-41d4-a716-446655440000"]
    (t/is (some? (d/parse-uuid uuid-str))))
  (t/is (nil? (d/parse-uuid "not-a-uuid")))
  (t/is (nil? (d/parse-uuid nil))))

(t/deftest coalesce-str-test
  (t/is (= "default" (d/coalesce-str nil "default")))
  ;; Numbers always stringify on both platforms
  (t/is (= "42" (d/coalesce-str 42 "default")))
  ;; ##NaN returns default on both platforms now that nan? is fixed on JVM
  (t/is (= "default" (d/coalesce-str ##NaN "default")))
  ;; Strings: in CLJS js/isNaN("hello")=true so "default" is returned;
  ;;          in CLJ nan? is false for strings so (str "hello")="hello" is returned.
  #?(:cljs (t/is (= "default" (d/coalesce-str "hello" "default"))))
  #?(:clj  (t/is (= "hello" (d/coalesce-str "hello" "default")))))

(t/deftest coalesce-test
  (t/is (= "hello" (d/coalesce "hello" "default")))
  (t/is (= "default" (d/coalesce nil "default")))
  ;; coalesce uses `or`, so false is falsy and returns the default
  (t/is (= "default" (d/coalesce false "default")))
  (t/is (= 42 (d/coalesce 42 0))))

(t/deftest read-string-test
  (t/is (= {:a 1} (d/read-string "{:a 1}")))
  (t/is (= [1 2 3] (d/read-string "[1 2 3]")))
  (t/is (= :keyword (d/read-string ":keyword"))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; String / keyword helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest name-test
  (t/is (= "foo" (d/name :foo)))
  (t/is (= "foo" (d/name "foo")))
  (t/is (nil? (d/name nil)))
  (t/is (= "42" (d/name 42))))

(t/deftest prefix-keyword-test
  (t/is (= :prefix-test (d/prefix-keyword "prefix-" :test)))
  (t/is (= :ns-id (d/prefix-keyword :ns- :id)))
  (t/is (= :ab (d/prefix-keyword "a" "b"))))

(t/deftest kebab-keys-test
  (t/is (= {:foo-bar 1 :baz-qux 2}
           (d/kebab-keys {"fooBar" 1 "bazQux" 2})))
  (t/is (= {:my-key {:nested-key 1}}
           (d/kebab-keys {:myKey {:nestedKey 1}}))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Utility helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest regexp-test
  (t/is (d/regexp? #"foo"))
  (t/is (not (d/regexp? "foo")))
  (t/is (not (d/regexp? nil))))

(t/deftest nilf-test
  (let [safe-inc (d/nilf inc)]
    (t/is (nil? (safe-inc nil)))
    (t/is (= 2 (safe-inc 1))))
  (let [safe-add (d/nilf +)]
    (t/is (nil? (safe-add 1 nil)))
    (t/is (= 3 (safe-add 1 2)))))

(t/deftest nilv-test
  (t/is (= "default" (d/nilv nil "default")))
  (t/is (= "value" (d/nilv "value" "default")))
  (t/is (= false (d/nilv false "default")))
  ;; transducer arity
  (t/is (= ["a" "default" "b"]
           (into [] (d/nilv "default") ["a" nil "b"]))))

(t/deftest any-key-test
  (t/is (d/any-key? {:a 1 :b 2} :a))
  (t/is (d/any-key? {:a 1 :b 2} :z :b))
  (t/is (not (d/any-key? {:a 1} :z :x))))

(t/deftest tap-test
  (let [received (atom nil)]
    (t/is (= [1 2 3] (d/tap #(reset! received %) [1 2 3])))
    (t/is (= [1 2 3] @received))))

(t/deftest tap-r-test
  (let [received (atom nil)]
    (t/is (= [1 2 3] (d/tap-r [1 2 3] #(reset! received %))))
    (t/is (= [1 2 3] @received))))

(t/deftest map-diff-test
  ;; identical maps produce empty diff
  (t/is (= {} (d/map-diff {:a 1} {:a 1})))
  ;; changed value
  (t/is (= {:a [1 2]} (d/map-diff {:a 1} {:a 2})))
  ;; removed key
  (t/is (= {:b [2 nil]} (d/map-diff {:a 1 :b 2} {:a 1})))
  ;; added key
  (t/is (= {:c [nil 3]} (d/map-diff {:a 1} {:a 1 :c 3})))
  ;; nested diff
  (t/is (= {:b {:c [1 2]}} (d/map-diff {:b {:c 1}} {:b {:c 2}}))))

(t/deftest unique-name-test
  ;; name not in used set — returned as-is
  (t/is (= "foo" (d/unique-name "foo" #{})))
  ;; name already used — append counter
  (t/is (= "foo-1" (d/unique-name "foo" #{"foo"})))
  (t/is (= "foo-2" (d/unique-name "foo" #{"foo" "foo-1"})))
  ;; name already has numeric suffix
  (t/is (= "foo-2" (d/unique-name "foo-1" #{"foo-1"})))
  ;; prefix-first? mode — skips foo-1 (counter=1 returns bare prefix)
  ;; so with #{} not used, still returns "foo"
  (t/is (= "foo" (d/unique-name "foo" #{} true)))
  ;; with prefix-first? and "foo" used, counter=1 produces "foo" again (used),
  ;; so jumps to counter=2 → "foo-2"
  (t/is (= "foo-2" (d/unique-name "foo" #{"foo"} true))))

(t/deftest toggle-selection-test
  ;; without toggle, always returns set with just the value
  (let [s (d/ordered-set :a :b)]
    (t/is (= (d/ordered-set :c) (d/toggle-selection s :c))))
  ;; with toggle=true, adds if not present
  (let [s (d/ordered-set :a)]
    (t/is (contains? (d/toggle-selection s :b true) :b)))
  ;; with toggle=true, removes if already present
  (let [s (d/ordered-set :a :b)]
    (t/is (not (contains? (d/toggle-selection s :a true) :a)))))

(t/deftest invert-map-test
  (t/is (= {1 :a 2 :b} (d/invert-map {:a 1 :b 2})))
  (t/is (= {} (d/invert-map {}))))

(t/deftest obfuscate-string-test
  ;; short string (< 10) — all stars
  (t/is (= "****" (d/obfuscate-string "abcd")))
  ;; long string — first 5 chars kept
  (t/is (= "hello*****" (d/obfuscate-string "helloworld")))
  ;; full? mode
  (t/is (= "***" (d/obfuscate-string "abc" true)))
  ;; empty string
  (t/is (= "" (d/obfuscate-string ""))))

(t/deftest unstable-sort-test
  (t/is (= [1 2 3 4] (d/unstable-sort [3 1 4 2])))
  ;; In CLJS, garray/sort requires a comparator returning -1/0/1 (not boolean).
  ;; Use compare with reversed args for descending sort on both platforms.
  (t/is (= [4 3 2 1] (d/unstable-sort #(compare %2 %1) [3 1 4 2])))
  ;; Empty collection: CLJ returns '(), CLJS returns nil (from seq on [])
  (t/is (empty? (d/unstable-sort []))))

(t/deftest opacity-to-hex-test
  ;; opacity-to-hex uses JavaScript number methods (.toString 16 / .padStart)
  ;; so it only produces output in CLJS environments.
  #?(:cljs (t/is (= "ff" (d/opacity-to-hex 1))))
  #?(:cljs (t/is (= "00" (d/opacity-to-hex 0))))
  #?(:cljs (t/is (= "80" (d/opacity-to-hex (/ 128 255)))))
  #?(:clj (t/is true "opacity-to-hex is CLJS-only")))

(t/deftest format-precision-test
  (t/is (= "12" (d/format-precision 12.0123 0)))
  (t/is (= "12" (d/format-precision 12.0123 1)))
  (t/is (= "12.01" (d/format-precision 12.0123 2)))
  (t/is (= "12.012" (d/format-precision 12.0123 3)))
  (t/is (= "0.1" (d/format-precision 0.1 2))))

(t/deftest format-number-test
  (t/is (= "3.14" (d/format-number 3.14159)))
  (t/is (= "3" (d/format-number 3.0)))
  (t/is (= "3.14" (d/format-number "3.14159")))
  (t/is (nil? (d/format-number nil)))
  (t/is (= "3.1416" (d/format-number 3.14159 {:precision 4}))))

(t/deftest append-class-test
  (t/is (= "foo bar" (d/append-class "foo" "bar")))
  (t/is (= "bar" (d/append-class nil "bar")))
  ;; empty string is treated like nil — no leading space
  (t/is (= "bar" (d/append-class "" "bar"))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Additional helpers (5th batch)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(t/deftest not-empty-predicate
  (t/is (d/not-empty? [1 2 3]))
  (t/is (d/not-empty? {:a 1}))
  (t/is (d/not-empty? "abc"))
  (t/is (not (d/not-empty? [])))
  (t/is (not (d/not-empty? {})))
  (t/is (not (d/not-empty? nil))))

(t/deftest editable-collection-predicate
  (t/is (d/editable-collection? []))
  (t/is (d/editable-collection? [1 2 3]))
  (t/is (d/editable-collection? {}))
  (t/is (d/editable-collection? {:a 1}))
  (t/is (not (d/editable-collection? nil)))
  (t/is (not (d/editable-collection? "hello")))
  (t/is (not (d/editable-collection? 42))))

(t/deftest num-predicate-2
  (t/is (d/num? 1))
  (t/is (d/num? 0))
  (t/is (d/num? -3.14))
  (t/is (not (d/num? ##NaN)))
  (t/is (not (d/num? ##Inf)))
  (t/is (not (d/num? nil)))
  ;; In CLJS, js/isFinite coerces strings → (d/num? "1") is true on CLJS, false on JVM
  #?(:clj (t/is (not (d/num? "1"))))
  ;; multi-arity
  (t/is (d/num? 1 2))
  (t/is (d/num? 1 2 3))
  (t/is (d/num? 1 2 3 4))
  (t/is (d/num? 1 2 3 4 5))
  (t/is (not (d/num? 1 ##NaN)))
  (t/is (not (d/num? 1 2 ##Inf)))
  (t/is (not (d/num? 1 2 3 ##NaN)))
  (t/is (not (d/num? 1 2 3 4 ##Inf))))

(t/deftest num-string-predicate
  ;; num-string? returns true for strings that represent valid numbers
  (t/is (d/num-string? "42"))
  (t/is (d/num-string? "3.14"))
  (t/is (d/num-string? "-7"))
  (t/is (not (d/num-string? "hello")))
  (t/is (not (d/num-string? nil)))
  ;; non-string types always return false
  (t/is (not (d/num-string? 42)))
  (t/is (not (d/num-string? :keyword)))
  ;; In CLJS, js/isNaN("") → false (empty string coerces to 0), so "" is numeric
  #?(:clj (t/is (not (d/num-string? ""))))
  #?(:cljs (t/is (d/num-string? ""))))

(t/deftest percent-predicate
  (t/is (d/percent? "50%"))
  (t/is (d/percent? "100%"))
  (t/is (d/percent? "0%"))
  (t/is (d/percent? "3.5%"))
  ;; percent? uses str/rtrim which strips the trailing % then checks numeric,
  ;; so a plain numeric string without % also returns true
  (t/is (d/percent? "50"))
  (t/is (not (d/percent? "abc%")))
  (t/is (not (d/percent? "abc"))))

(t/deftest parse-percent-test
  (t/is (= 0.5 (d/parse-percent "50%")))
  (t/is (= 1.0 (d/parse-percent "100%")))
  (t/is (= 0.0 (d/parse-percent "0%")))
  ;; falls back to parse-double when no % suffix
  (t/is (= 0.75 (d/parse-percent "0.75")))
  ;; invalid value returns default
  (t/is (nil? (d/parse-percent "abc%")))
  (t/is (= 0.0 (d/parse-percent "abc%" 0.0))))

(t/deftest lazy-map-test
  (let [calls (atom 0)
        m     (d/lazy-map [:a :b :c]
                          (fn [k]
                            (swap! calls inc)
                            (name k)))]
    ;; The map has the right keys
    (t/is (= #{:a :b :c} (set (keys m))))
    ;; Values are delays — force them
    (t/is (= "a" @(get m :a)))
    (t/is (= "b" @(get m :b)))
    (t/is (= "c" @(get m :c)))))

(t/deftest oreorder-before-test
  ;; No ks path: insert k v before before-k in a flat ordered-map
  (let [om (d/ordered-map :a 1 :b 2 :c 3)
        result (d/oreorder-before om [] :d 4 :b)]
    (t/is (= [:a :d :b :c] (vec (keys result)))))
  ;; before-k not found → appended at end
  (let [om (d/ordered-map :a 1 :b 2)
        result (d/oreorder-before om [] :c 3 :z)]
    (t/is (= [:a :b :c] (vec (keys result)))))
  ;; nil before-k → appended at end
  (let [om (d/ordered-map :a 1 :b 2)
        result (d/oreorder-before om [] :c 3 nil)]
    (t/is (= [:a :b :c] (vec (keys result)))))
  ;; existing key k is removed from its old position
  (let [om (d/ordered-map :a 1 :b 2 :c 3)
        result (d/oreorder-before om [] :c 99 :a)]
    (t/is (= [:c :a :b] (vec (keys result))))))

(t/deftest oassoc-in-before-test
  ;; Simple case: add a new key before an existing key
  (let [om (d/ordered-map :a 1 :b 2 :c 3)
        result (d/oassoc-in-before om [:b] [:x] 99)]
    (t/is (= [:a :x :b :c] (vec (keys result))))
    (t/is (= 99 (get result :x))))
  ;; before-k not found → oassoc-in behaviour (append)
  (let [om (d/ordered-map :a 1 :b 2)
        result (d/oassoc-in-before om [:z] [:x] 99)]
    (t/is (= 99 (get result :x)))))

(t/deftest reorder-test
  ;; Move element from index 0 to position between index 2 and 3
  (t/is (= [:b :c :a :d] (d/reorder [:a :b :c :d] 0 3)))
  ;; Move last element to the front
  (t/is (= [:d :a :b :c] (d/reorder [:a :b :c :d] 3 0)))
  ;; No-op: same logical position (from-pos == to-space-between-pos)
  (t/is (= [:a :b :c :d] (d/reorder [:a :b :c :d] 1 1)))
  ;; Clamp out-of-range positions
  (t/is (= [:b :c :d :a] (d/reorder [:a :b :c :d] 0 100)))
  (t/is (= [:a :b :c :d] (d/reorder [:a :b :c :d] -5 0))))
