             [structural-typing.predicates :as p])
   (:require [com.rpl.specter :refer [ALL]])
   (:use midje.sweet))
+
+;;; The core
+
+;; for a little clarity in the more complicated cases
+(def type-has vector)
+
+
+;; ..t.. is an irrelevant type-rep
+(fact "type descriptions that are maps"
+  (fact "simple maps just have their keys and values vectorized"
+    (subject/canonicalize ..t.. {:a p/must-exist}) => {[:a] [p/must-exist]}
+    (subject/canonicalize ..t.. {:a p/must-exist, :b p/must-exist})
+    => {[:a] [p/must-exist]
+        [:b] [p/must-exist]})
+  
+  (fact "already vectorized keys and values are left alone"
+    (subject/canonicalize ..t.. {[:a] p/must-exist}) => {[:a] [p/must-exist]}
+    (subject/canonicalize ..t.. {[:a] [p/must-exist]}) => {[:a] [p/must-exist]}
+    (subject/canonicalize ..t.. {[:a :b] p/must-exist}) => {[:a :b] [p/must-exist]})
+
+  (fact "nested maps have keys flattened into paths"
+    (subject/canonicalize ..t.. {:a {:b p/must-exist}}) => {[:a :b] [p/must-exist]}))
+
+(fact "type descriptions that are vectors"
+  (fact "vectors are shorthand for maps with `p/must-exist`"
+    (subject/canonicalize ..t.. [ :a :b ]) => {[:a] [p/must-exist] [:b] [p/must-exist]})
+  (fact "alternate for above - for clarity"
+    (subject/canonicalize ..t.. (type-has :a :b)) => {[:a] [p/must-exist] [:b] [p/must-exist]})
+  (fact "single-element vectors are the same as atoms"
+    (subject/canonicalize ..t.. (type-has [:a] [:b] ))
+    => {[:a] [p/must-exist] [:b] [p/must-exist]})
+
+  (fact "multi-element vectors are paths"
+    (subject/canonicalize ..t.. (type-has [:a :c]
+                                          [:b :d])) => {[:a :c] [p/must-exist]
+                                                        [:b :d] [p/must-exist]})
+  
+  (fact "nested and unnested elements can be combined"
+    (subject/canonicalize ..t.. (type-has :a [:b :c] :d)) => {[:a] [p/must-exist]
+                                                              [:b :c] [p/must-exist]
+                                                              [:d] [p/must-exist]})
+
+  (fact "double-nesting indicates alternatives"
+    (subject/canonicalize ..t.. (type-has :a [:b [1 2] :c] :d)) => {[:a] [p/must-exist]
+                                                                    [:b 1 :c] [p/must-exist]
+                                                                    [:b 2 :c] [p/must-exist]
+                                                                    [:d] [p/must-exist]}
+
+    (subject/canonicalize ..t.. (type-has [[:a :b]])) => {[:a] [p/must-exist]
+                                                          [:b] [p/must-exist]})
+
+
+  (fact "maps within vectors contribute paths - only paths - that must exist"
+    (subject/canonicalize ..t.. [ {:a #'even?, :b #'even?} ]) => {[:a] [p/must-exist]
+                                                                  [:b] [p/must-exist]}
+    (subject/canonicalize ..t.. [ [:a {:b {:c #'even?}
+                                       :d #'even?}] ]) => {[:a :b :c] [p/must-exist]
+                                                           [:a :d] [p/must-exist]}
+
+      (subject/canonicalize ..t.. [ [:a {:b #'even?} :e] ]) => (throws)))
+
+(fact "signifiers can only be maps and vectors"
+  (subject/canonicalize ..t.. :a) => (throws #"maps or vectors"))
+
+(fact "multiple arguments are allowed"
+  (subject/canonicalize ..t.. {:a p/must-exist} {:b p/must-exist}) => {[:a] [p/must-exist]
+                                                                       [:b] [p/must-exist]}
+
+  (fact "arguments with the same keys have their values merged"
+    (subject/canonicalize ..t.. {:a p/must-exist} {:a #'even?}) => {[:a] [p/must-exist #'even?]}
+    (subject/canonicalize ..t.. {:a {:b p/must-exist}}
+                          {:a #'map?}
+                          {:a {:b #'even?}}) => {[:a] [#'map?]
+                                                 [:a :b] [p/must-exist #'even?]})
+
+  (fact "maps and vectors can be merged"
+    (subject/canonicalize ..t.. [ :a :b ] {:a #'even?}) => {[:a] [p/must-exist #'even?]
+                                                            [:b] [p/must-exist]}))
+
+(fact "And here's an example of various ways to talk about a nested structure"
+  ;; A figure has a color and many points
+  (subject/canonicalize ..t.. [ :color
+                               [:points ALL :x]
+                               [:points ALL :y] ]) => {[:color] [p/must-exist]
+                                                       [:points ALL :x] [p/must-exist]
+                                                       [:points ALL :y] [p/must-exist]}
+                               
+  (subject/canonicalize ..t.. [:color
+                               [:points ALL [:x :y]]]) => {[:color] [p/must-exist]
+                                                           [:points ALL :x] [p/must-exist]
+                                                           [:points ALL :y] [p/must-exist]}
+
+  (fact "maps can contribute to the `required` list"
+    (let [point {[:x] [#'integer?]
+                 [:y] [#'integer?]}]
+      ;; Here we require x and y to exist, but lose the requirement they be integers.
+      (subject/canonicalize ..t.. [:color [:points ALL point]])
+      => {[:color] [p/must-exist]
+          [:points ALL :x] [p/must-exist]
+          [:points ALL :y] [p/must-exist]}
+        
+      ;; To get those options back, we use a map.
+      (subject/canonicalize ..t.. [:color [:points ALL point]]
+                            {:color #'string?
+                             [:points ALL] point})
+      => {[:color] [p/must-exist #'string?]
+          [:points ALL :x] [p/must-exist #'integer?]
+          [:points ALL :y] [p/must-exist #'integer?]})
+
+
+    ;; If a nested type has required keys, those are preserved.
+    (let [required-xy-point {[:x] [p/must-exist #'integer?]
+                             [:y] [p/must-exist #'integer?]}]
+      ;; As above, a mention in the required list transfers required-ness
+      (subject/canonicalize ..t.. [:color [:points ALL required-xy-point]])
+      => {[:color] [p/must-exist]
+          [:points ALL :x] [p/must-exist]
+          [:points ALL :y] [p/must-exist]}
+      
+      ;; To get those options back, we use a map.
+      (subject/canonicalize ..t.. [:color]
+                            {:color #'string?
+                             [:points ALL] required-xy-point})
+      => {[:color] [p/must-exist #'string?]
+          [:points ALL :x] [p/must-exist #'integer?]
+          [:points ALL :y] [p/must-exist #'integer?]})))
+
+(fact "this is a pretty unlikely map to use, but it will work"
+  (subject/canonicalize ..t..
+                        {:points {ALL {:x #'integer?
+                                       :y #'integer?}}}) => {[:points ALL :x] [#'integer?]
+                                                             [:points ALL :y] [#'integer?]})
+
+
+
+
+
+;;;; Utilities
 
 
 (facts "nested-map->path-map flattens nested keys into a vector of keys"




;; for a little clarity in the more complicated cases
(def type-has vector)


;; ..t.. is an irrelevant type-rep
(fact "various signifiers can be converted into a canonical form"
  (fact "signifiers that are maps"
    (fact "simple maps just have their keys and values vectorized"
      (subject/canonicalize ..t.. {:a p/must-exist}) => {[:a] [p/must-exist]}
      (subject/canonicalize ..t.. {:a p/must-exist, :b p/must-exist})
      => {[:a] [p/must-exist]
          [:b] [p/must-exist]})
    
    (fact "already vectorized keys and values are left alone"
      (subject/canonicalize ..t.. {[:a] p/must-exist}) => {[:a] [p/must-exist]}
      (subject/canonicalize ..t.. {[:a] [p/must-exist]}) => {[:a] [p/must-exist]}
      (subject/canonicalize ..t.. {[:a :b] p/must-exist}) => {[:a :b] [p/must-exist]})

    (fact "nested maps"
      (subject/canonicalize ..t.. {:a {:b p/must-exist}}) => {[:a :b] [p/must-exist]}))

  (fact "signifiers that are vectors"
    (fact "vectors are shorthand for maps with `p/must-exist`"
      (subject/canonicalize ..t.. [ :a :b ]) => {[:a] [p/must-exist] [:b] [p/must-exist]})
    (fact "alternate for above - for clarity"
      (subject/canonicalize ..t.. (type-has :a :b)) => {[:a] [p/must-exist] [:b] [p/must-exist]})
    (fact "single-element vectors are the same as atoms"
      (subject/canonicalize ..t.. (type-has [:a] [:b] ))
      => {[:a] [p/must-exist] [:b] [p/must-exist]})

    (fact "multi-element vectors are paths"
      (subject/canonicalize ..t.. (type-has [:a :c]
                                            [:b :d])) => {[:a :c] [p/must-exist]
                                                          [:b :d] [p/must-exist]})
    
    (fact "nested and unnested can be combined"
      (subject/canonicalize ..t.. (type-has :a [:b :c] :d)) => {[:a] [p/must-exist]
                                                                [:b :c] [p/must-exist]
                                                                [:d] [p/must-exist]})

    (fact "double-nesting indicates alternatives"
      (subject/canonicalize ..t.. (type-has :a [:b [1 2] :c] :d)) => {[:a] [p/must-exist]
                                                                      [:b 1 :c] [p/must-exist]
                                                                      [:b 2 :c] [p/must-exist]
                                                                      [:d] [p/must-exist]}

      (subject/canonicalize ..t.. (type-has [[:a :b]])) => {[:a] [p/must-exist]
                                                          [:b] [p/must-exist]})




    (fact "maps within vectors contribute paths - only paths - that must exist"
      (subject/canonicalize ..t.. [ {:a #'even?, :b #'even?} ]) => {[:a] [p/must-exist]
                                                                    [:b] [p/must-exist]}
      (subject/canonicalize ..t.. [ [:a {:b {:c #'even?}
                                         :d #'even?}] ]) => {[:a :b :c] [p/must-exist]
                                                             [:a :d] [p/must-exist]}

      (subject/canonicalize ..t.. [ [:a {:b #'even?} :e] ]) => (throws)))

  (fact "signifiers can only be maps and vectors"
    (subject/canonicalize ..t.. :a) => (throws #"maps or vectors"))

  (fact "multiple arguments are allowed"
    (subject/canonicalize ..t.. {:a p/must-exist} {:b p/must-exist}) => {[:a] [p/must-exist]
                                                                         [:b] [p/must-exist]}

    (fact "arguments with the same keys have their values merged"
      (subject/canonicalize ..t.. {:a p/must-exist} {:a #'even?}) => {[:a] [p/must-exist #'even?]}
      (subject/canonicalize ..t.. {:a {:b p/must-exist}}
                            {:a #'map?}
                            {:a {:b #'even?}}) => {[:a] [#'map?]
                                                   [:a :b] [p/must-exist #'even?]})

    (fact "maps and vectors can be merged"
      (subject/canonicalize ..t.. [ :a :b ] {:a #'even?}) => {[:a] [p/must-exist #'even?]
                                                              [:b] [p/must-exist]}))

  (fact "And here's an example of various ways to talk about a nested structure"
    ;; A figure has a color and many points
    (subject/canonicalize ..t.. [ :color
                           [:points ALL :x]
                           [:points ALL :y] ]) => {[:color] [p/must-exist]
                                                   [:points ALL :x] [p/must-exist]
                                                   [:points ALL :y] [p/must-exist]}

    (subject/canonicalize ..t.. [:color
                           [:points ALL [:x :y]]]) => {[:color] [p/must-exist]
                                                       [:points ALL :x] [p/must-exist]
                                                       [:points ALL :y] [p/must-exist]}


    (fact "maps can contribute to the `required` list"
      (let [point {[:x] [#'integer?]
                   [:y] [#'integer?]}]
        ;; Here we require x and y to exist, but lose the requirement they be integers.
        (subject/canonicalize ..t.. [:color [:points ALL point]])
        => {[:color] [p/must-exist]
            [:points ALL :x] [p/must-exist]
            [:points ALL :y] [p/must-exist]}
        
        ;; To get those options back, we use a map.
        (subject/canonicalize ..t.. [:color [:points ALL point]]
                              {:color #'string?
                               [:points ALL] point})
        => {[:color] [p/must-exist #'string?]
            [:points ALL :x] [p/must-exist #'integer?]
            [:points ALL :y] [p/must-exist #'integer?]})


      ;; If a nested type has required keys, those are preserved.
      (let [required-xy-point {[:x] [p/must-exist #'integer?]
                               [:y] [p/must-exist #'integer?]}]
        ;; As above, a mention in the required list transfers required-ness
        (subject/canonicalize ..t.. [:color [:points ALL required-xy-point]])
        => {[:color] [p/must-exist]
            [:points ALL :x] [p/must-exist]
            [:points ALL :y] [p/must-exist]}
        
        ;; To get those options back, we use a map.
        (subject/canonicalize ..t.. [:color]
                              {:color #'string?
                               [:points ALL] required-xy-point})
        => {[:color] [p/must-exist #'string?]
            [:points ALL :x] [p/must-exist #'integer?]
            [:points ALL :y] [p/must-exist #'integer?]})))


  (fact "this is a pretty unlikely map to use, but it will work"
    (subject/canonicalize ..t..
       {:points {ALL {:x #'integer?
                      :y #'integer?}}}) => {[:points ALL :x] [#'integer?]
                                            [:points ALL :y] [#'integer?]})
  )





;; ;; Rework into canonicalize ..t..
;;    (subject/expand-path-shorthand :x) => [:x]

      ;; (subject/expand-path-shorthand {:a #'even? :b #'even?})
      ;; =future=> (just [:a] [:b] :in-any-order)
      ;; (subject/expand-path-shorthand {:a {:b #'even?} :c #'even?})
      ;; =future=> (just [:a :b] [:c] :in-any-order)
;; (future-fact "three-argument form allows lookup in a type-map"
;;     (let [type-map {:Point {[:x] [p/must-exist #'integer?]
;;                             [:y] [p/must-exist #'integer?]}}]


  (fact "top level non-collections are canonicalized"
    (subject/any-required-seq->maps [:a :b]) => {[:a] [p/must-exist]
                                                 [:b] [p/must-exist]}))

;;; Types can be expanded by signifier

(facts expand-type-finders
  (subject/expand-type-finders {} []) => []
