 (def compute-log (atom []))
 (defn log! [id] (swap! compute-log conj id))
 
+(defn at-least-n-in [thing n seq]
+  (<= n (count (filter #(= % thing) seq))))
+
 (t/use-fixtures :each (fn [f] (reset! compute-log []) (f)))
 
+(def kbase   (str "base---" (java.util.UUID/randomUUID)))
+(def kinc    (str "inc----" (java.util.UUID/randomUUID)))
+(def kas-map (str "as-map-" (java.util.UUID/randomUUID)))
+(def ksum    (str "sum----" (java.util.UUID/randomUUID)))
+
 (defn test-spec [base]
-  {:base   [[]           base]
-   :inc    [[:base]      (fn [base] (log! :inc) (inc base))]
-   :as-map [[:base :inc] (fn [base inc] (log! :as-map) {:base base :after-inc inc})]
-   :sum    [[:as-map]    (fn [as-map] (log! :sum) (+ (:base as-map) (:after-inc as-map)))]})
-
+  {kbase   [[]           base]
+   kinc    [[kbase]      (fn [base] (log! kinc) (inc base))]
+   kas-map [[kbase kinc] (fn [base inc] (log! kas-map) {:base base :after-inc inc})]
+   ksum    [[kas-map]    (fn [as-map] (log! ksum) (+ (:base as-map) (:after-inc as-map)))]})
 
 (t/deftest not-required-test
   (t/is (= [2] (drv/not-required {:a 1 :b 2} #{:a})))


(t/deftest build-test
  (let [drvs (drv/build (test-spec (atom 1)))]
    (t/is (= 1 @(:base drvs)))
    (t/is (= 2 @(:inc drvs)))
    (t/is (= {:base 1 :after-inc 2} @(:as-map drvs)))
    (t/is (= 3 @(:sum drvs)))))

(t/deftest sync-derivatives-test
  (let [drvs (drv/sync-derivatives! (test-spec (atom 1)) (drv/prefix-id) {} [:base :inc])]
    (t/is (= 1 @(:base drvs)))
    (t/is (= 2 @(:inc drvs)))
    (t/is (nil? (:as-map drvs)))
    (t/is (nil? (:sum drvs)))
    (t/is (= [:inc] @compute-log))))

(t/deftest sync-derivatives-test
  (let [spec (test-spec (atom 1))
        id   (drv/prefix-id)
        drvs (drv/sync-derivatives! spec id {} [:base :inc])]
    (t/is (= 1 @(:base drvs)))
    (t/is (= 2 @(:inc drvs)))
    (t/is (nil? (:as-map drvs)))
    (t/is (nil? (:sum drvs)))
    (t/is (= [:inc] @compute-log))
    ;; sync with more required keys
    (let [drvs-updated (drv/sync-derivatives! spec id drvs [:base :inc :as-map])]
      (t/is (= [:inc :as-map] @compute-log)))))

;; This test will fail if watches are not properly removed from source refs
(t/deftest watches-disposed-test
  (let [base  (atom 1)
        spec  (test-spec base)
        id    (drv/prefix-id)
        drvs1 (drv/sync-derivatives! spec id {} [:base :inc])
        drvs2 (drv/sync-derivatives! spec id drvs1 [:base])]
    (swap! base inc)
    (t/is (= [:inc] @compute-log))))

(t/deftest pool-test
  (let [base  (atom 1)
        spec  (test-spec base)
        {:keys [get! release!]} (drv/derivatives-pool spec)]
    (t/is (= 1 @(get! :base :1-token)))
    (t/is (= [] @compute-log))
    (t/is (= 2 @(get! :inc :2-token)))
    (t/is (= [:inc] @compute-log))
    (t/is (= 3 @(get! :sum :3-token)))
    (t/is (= [:inc :as-map :sum] @compute-log))
    (swap! base inc)
    (t/is (= [:inc :as-map :sum :inc :as-map :sum :as-map] @compute-log))
    (t/is (= 5 @(get! :sum :3-token)))))

(t/deftest pool-throws-if-unknown-test
  (let [base  (atom 1)

        pool-state (atom {})
        dm (drv/->DerivativesPool spec (drv/prefix-id) (drv/spec->graph spec) pool-state)]

    (drv/get! dm ksum :1-token)
    (t/is (= {ksum #{:1-token}} (:registry @pool-state)))
    (drv/get! dm kas-map :2-token)
    (t/is (= {ksum #{:1-token} kas-map #{:2-token}} (:registry @pool-state)))
    (t/is (= {:base 1 :after-inc 2} (-> @pool-state :derivatives (get kas-map) deref)))
    (t/is (= 3 (-> @pool-state :derivatives (get ksum) deref)))

    ;; :as-map still needs to be present because :sum requires it
    (drv/release! dm kas-map :2-token)
    (t/is (= {ksum #{:1-token}} (:registry @pool-state)))
    (t/is (= {:base 1 :after-inc 2} (-> @pool-state :derivatives (get kas-map) deref)))
    (t/is (= 3 (-> @pool-state :derivatives (get ksum) deref)))

    ;; After releasing all, pool and registry should be empty
    (drv/release! dm ksum :1-token)
    (t/is (= {} (:registry @pool-state)))
    (t/is (= {} (:derivatives @pool-state)))))

