(ns frontend.db.query-dsl-test
  (:require [frontend.db.query-dsl :as dsl]
            [frontend.db :as db]
            [frontend.db.conn :as conn]
            [frontend.db.config :refer [test-db] :as config]
            [datascript.core :as d]
            [frontend.db-schema :as schema]
            [frontend.handler.repo :as repo-handler]
            [promesa.core :as p]
            [cljs.test :refer [deftest is are testing use-fixtures]]))

;; TODO: quickcheck
;; 1. generate query filters
;; 2. find illegal queries which can't be executed by datascript
;; 3. find filters combinations which might break the current query implementation

(defn import-test-data!
  []
  (let [files [{:file/path "journals/2020_12_26.md"
                :file/content "---
title: Dec 26th, 2020
tags: [[page-tag-1]], page-tag-2
parent: [[child page 1]]
---
## DONE 26-b1 [[page 1]]
:PROPERTIES:
:created_at: 1608968448113
:last_modified_at: 1608968448113
:prop_a: val_a
:prop_c: [[page a]], [[page b]], [[page c]]
:END:
## LATER 26-b2-modified-later [[page 2]] #tag1
:PROPERTIES:
:created_at: 1608968448114
:last_modified_at: 1608968448120
:prop_b: val_b
:END:
## DONE [#A] 26-b3 [[page 1]]
:PROPERTIES:
:created_at: 1608968448115
:last_modified_at: 1608968448115
:END:
"}
               {:file/path "journals/2020_12_27.md"
                :file/content "---
title: Dec 27th, 2020
tags: page-tag-2, [[page-tag-3]]
parent: [[child page 1]], child page 2
---
## NOW [#A] b1 [[page 1]]
:PROPERTIES:
:created_at: 1609052958714
:last_modified_at: 1609052958714
:END:
## LATER [#B] b2-modified-later [[page 2]]
:PROPERTIES:
:created_at: 1609052959376
:last_modified_at: 1609052974285
:END:
## b3 [[page 1]]
:PROPERTIES:
:created_at: 1609052959954
:last_modified_at: 1609052959954
:prop_a: val_a
:END:
## b4 [[page 2]]
:PROPERTIES:
:created_at: 1609052961569
:last_modified_at: 1609052961569
:END:
## b5
:PROPERTIES:
:created_at: 1609052963089
:last_modified_at: 1609052963089
:END:"}
               {:file/path "journals/2020_12_28.md"
                :file/content "---
title: Dec 28th, 2020
parent: child page 2
---
## 28-b1 [[page 1]]
:PROPERTIES:
:created_at: 1609084800000
:last_modified_at: 1609084800000
:END:
## 28-b2-modified-later [[page 2]]
:PROPERTIES:
:created_at: 1609084800001
:last_modified_at: 1609084800020
:END:
## 28-b3 [[page 1]]
:PROPERTIES:
:created_at: 1609084800002
:last_modified_at: 1609084800002
:END:
"}]]
    (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false})))

(def parse (partial dsl/parse test-db))

(defn- q
  [s]
  (db/clear-query-state!)
  (let [parse-result (parse s)
        query (:query parse-result)]
    {:query query
     :result (dsl/query test-db s)}))

(defn q-count
  [s]
  (let [{:keys [query result]} (q s)]
    {:query query
     :count (if result
              (count @result)
              0)}))

(defn count-only
  [s]
  (:count (q-count s)))

(defonce empty-result {:query nil :result nil})

(deftest test-parse
  []
  (testing "nil or blank strings should be ignored"
    (are [x y] (= (q x) y)
      nil empty-result
      "" empty-result
      " " empty-result))

  (testing "Non exists page should be ignored"
    (are [x y] (nil? (:result (q x)))
      "[[page-not-exist]]" empty-result
      "[[another-page-not-exist]]" empty-result))

  (testing "Single page query"
    (are [x y] (= (q-count x) y)
      "[[page 1]]"
      {:query '[[?b :block/path-ref-pages [:page/name "page 1"]]]
       :count 6}

      "[[page 2]]"
      {:query '[[?b :block/path-ref-pages [:page/name "page 2"]]]
       :count 4}))

  (testing "Block properties query"
    (are [x y] (= (q-count x) y)
      "(property prop_a val_a)"
      {:query '[[?b :block/properties ?prop]
                [(get ?prop "prop_a") ?v]
                (or
                 [(= ?v "val_a")]
                 [(contains? ?v "val_a")])]
       :count 2}

      "(property prop_b val_b)"
      {:query '[[?b :block/properties ?prop]
                [(get ?prop "prop_b") ?v]
                (or
                 [(= ?v "val_b")]
                 [(contains? ?v "val_b")])]
       :count 1}

      "(and (property prop_b val_b))"
      {:query '[[?b :block/properties ?prop]
                [(get ?prop "prop_b") ?v]
                (or
                 [(= ?v "val_b")]
                 [(contains? ?v "val_b")])]
       :count 1}

      "(and (property prop_c \"page c\"))"
      {:query '[[?b :block/properties ?prop]
                [(get ?prop "prop_c") ?v]
                (or
                 [(= ?v "page c")]
                 [(contains? ?v "page c")])]
       :count 1}

      ;; TODO: optimize
      "(and (property prop_c \"page c\") (property prop_c \"page b\"))"
      {:query '([?b :block/properties ?prop]
                [(get ?prop "prop_c") ?v]
                (or [(= ?v "page c")] [(contains? ?v "page c")])
                [(get ?prop "prop_c") ?v1]
                (or [(= ?v1 "page b")] [(contains? ?v1 "page b")]))
       :count 1}

      "(or (property prop_c \"page c\") (property prop_b val_b))"
      {:query '(or (and [?b :block/properties ?prop]
                        [(get ?prop "prop_c") ?v]
                        (or [(= ?v "page c")] [(contains? ?v "page c")]))
                   (and [?b :block/properties ?prop]
                        [(get ?prop "prop_b") ?v]
                        (or [(= ?v "val_b")] [(contains? ?v "val_b")])))
       :count 2}))

  (testing "TODO queries"
    (are [x y] (= (q-count x) y)
      "(todo now)"
      {:query '[[?b :block/marker ?marker]
                [(contains? #{"NOW"} ?marker)]]
       :count 1}

      "(todo NOW)"
      {:query '[[?b :block/marker ?marker]
                [(contains? #{"NOW"} ?marker)]]
       :count 1}

      "(todo later)"
      {:query '[[?b :block/marker ?marker]
                [(contains? #{"LATER"} ?marker)]]
       :count 2}

      "(todo now later)"
      {:query '[[?b :block/marker ?marker]
                [(contains? #{"NOW" "LATER"} ?marker)]]
       :count 3}

      "(todo [now later])"
      {:query '[[?b :block/marker ?marker]
                [(contains? #{"NOW" "LATER"} ?marker)]]
       :count 3}))

  (testing "Priority queries"
    (are [x y] (= (q-count x) y)
      "(priority A)"
      {:query '[[?b :block/priority ?priority]
                [(contains? #{"A"} ?priority)]]
       :count 2}

      "(priority a)"
      {:query '[[?b :block/priority ?priority]
                [(contains? #{"A"} ?priority)]]
       :count 2}

      "(priority a b)"
      {:query '[[?b :block/priority ?priority]
                [(contains? #{"A" "B"} ?priority)]]
       :count 3}

      "(priority [a b])"
      {:query '[[?b :block/priority ?priority]
                [(contains? #{"A" "B"} ?priority)]]
       :count 3}

      "(priority a b c)"
      {:query '[[?b :block/priority ?priority]
                [(contains? #{"A" "B" "C"} ?priority)]]
       :count 3}))

  (testing "all-page-tags queries"
    (are [x y] (= (q-count x) y)
      "(all-page-tags)"
      {:query '[[?e :page/tags ?p]]
       :count 3}))

  (testing "page-tags queries"
    (are [x y] (= (q-count x) y)
      "(page-tags [[page-tag-1]])"
      {:query '[[?p :page/tags ?t]
                [?t :page/name ?tag1]
                [(contains? #{"page-tag-1"} ?tag1)]]
       :count 1}

      "(page-tags page-tag-2)"
      {:query '[[?p :page/tags ?t]
                [?t :page/name ?tag1]
                [(contains? #{"page-tag-2"} ?tag1)]]
       :count 2}

      "(page-tags page-tag-1 page-tag-2)"
      {:query '[[?p :page/tags ?t]
                [?t :page/name ?tag1]
                [(contains? #{"page-tag-1" "page-tag-2"} ?tag1)]]
       :count 2}

      "(page-tags page-TAG-1 page-tag-2)"
      {:query '[[?p :page/tags ?t]
                [?t :page/name ?tag1]
                [(contains? #{"page-tag-1" "page-tag-2"} ?tag1)]]
       :count 2}

      "(page-tags [page-tag-1 page-tag-2])"
      {:query '[[?p :page/tags ?t]
                [?t :page/name ?tag1]
                [(contains? #{"page-tag-1" "page-tag-2"} ?tag1)]]
       :count 2}))

  (testing "page-property queries"
    (are [x y] (= (q-count x) y)
      "(page-property parent [[child page 1]])"
      {:query '[[?p :page/properties ?prop]
                [(get ?prop :parent) ?v]
                (or
                 [(= ?v "child page 1")]
                 [(contains? ?v "child page 1")])]
       :count 2}

      "(page-property parent \"child page 1\")"
      {:query '[[?p :page/properties ?prop]
                [(get ?prop :parent) ?v]
                (or
                 [(= ?v "child page 1")]
                 [(contains? ?v "child page 1")])]
       :count 2}

      "(and (page-property parent [[child page 1]]) (page-property parent [[child page 2]]))"
      {:query '([?p :page/properties ?prop]
                [(get ?prop :parent) ?v]
                (or [(= ?v "child page 1")] [(contains? ?v "child page 1")])
                (or [(= ?v "child page 2")] [(contains? ?v "child page 2")]))
       :count 1}

      "(or (page-property parent [[child page 1]]) (page-property parent [[child page 2]]))"
      {:query '(or (and
                    [?p :page/properties ?prop]
                    [(get ?prop :parent) ?v]
                    (or [(= ?v "child page 1")] [(contains? ?v "child page 1")]))
                   (and
                    [?p :page/properties ?prop]
                    [(get ?prop :parent) ?v]
                    (or [(= ?v "child page 2")] [(contains? ?v "child page 2")])))
       :count 3}))

  ;; boolean queries
  (testing "AND queries"
    (are [x y] (= (q-count x) y)
      "(and [[tag1]] [[page 2]])"
      {:query '([?b :block/path-ref-pages [:page/name "tag1"]]
                [?b :block/path-ref-pages [:page/name "page 2"]])
       :count 1})

    (are [x y] (= (q-count x) y)
      "(and [[tag1]] [[page 2]])"
      {:query '([?b :block/path-ref-pages [:page/name "tag1"]]
                [?b :block/path-ref-pages [:page/name "page 2"]])
       :count 1}))

  (testing "OR queries"
    (are [x y] (= (q-count x) y)
      "(or [[tag1]] [[page 2]])"
      {:query '(or
                (and [?b :block/path-ref-pages [:page/name "tag1"]])
                (and [?b :block/path-ref-pages [:page/name "page 2"]]))
       :count 4}))

  (testing "NOT queries"
    (are [x y] (= (q-count x) y)
      "(not [[page 1]])"
      {:query '([?b :block/uuid]
                (not [?b :block/path-ref-pages [:page/name "page 1"]]))
       :count 8}))

  (testing "Between query"
    (are [x y] (= (count-only x) y)
      "(and (todo now later done) (between [[Dec 26th, 2020]] tomorrow))"
      5

      ;; between with journal pages
      "(and (todo now later done) (between [[Dec 27th, 2020]] [[Dec 28th, 2020]]))"
      2

      ;; between with created_at
      "(and (todo now later done) (between created_at [[Dec 26th, 2020]] tomorrow))"
      5

      ;; between with last_modified_at
      "(and (todo now later done) (between last_modified_at [[Dec 26th, 2020]] tomorrow))"
      5))

  (testing "Nested boolean queries"
    (are [x y] (= (q-count x) y)
      "(and (todo done) (not [[page 1]]))"
      {:query '([?b :block/uuid]
                [?b :block/marker ?marker]
                [(contains? #{"DONE"} ?marker)]
                (not [?b :block/path-ref-pages [:page/name "page 1"]]))
       :count 0})

    (are [x y] (= (q-count x) y)
      "(and (todo now later) (or [[page 1]] [[page 2]]))"
      {:query '([?b :block/marker ?marker]
                [(contains? #{"NOW" "LATER"} ?marker)]
                (or (and [?b :block/path-ref-pages [:page/name "page 1"]])
                    (and [?b :block/path-ref-pages [:page/name "page 2"]])))
       :count 3})

    (are [x y] (= (q-count x) y)
      "(not (and (todo now later) (or [[page 1]] [[page 2]])))"
      {:query '([?b :block/uuid]
                (not
                 [?b :block/marker ?marker]
                 [(contains? #{"NOW" "LATER"} ?marker)]
                 (or
                  (and [?b :block/path-ref-pages [:page/name "page 1"]])
                  (and [?b :block/path-ref-pages [:page/name "page 2"]]))))
       :count 11})

    ;; FIXME: not working
    ;; (are [x y] (= (q-count x) y)
    ;;   "(or (priority a) (not (priority a)))"
    ;;   {:query '(or
    ;;             (and [?b :block/priority ?priority] [(contains? #{"A"} ?priority)])
    ;;             (and (not [?b :block/priority ?priority]
    ;;                       [(contains? #{"A"} ?priority)])))
    ;;    :count 5})

    (are [x y] (= (q-count x) y)
      "(and (todo now later done) (or [[page 1]] (not [[page 1]])))"
      {:query '([?b :block/uuid]
                [?b :block/marker ?marker]
                [(contains? #{"NOW" "LATER" "DONE"} ?marker)]
                (or
                 (and [?b :block/path-ref-pages [:page/name "page 1"]])
                 (and (not [?b :block/path-ref-pages [:page/name "page 1"]]))))
       :count 5}))

  (testing "sort-by (created_at defaults to desc)"
    (db/clear-query-state!)
    (let [result (->> (q "(and (todo now later done)
                               (sort-by created_at))")
                      :result
                      deref
                      (map #(get-in % [:block/properties "created_at"])))]
      (is (= result
             '(1609052959376 1609052958714 1608968448115 1608968448114 1608968448113)))))

  (testing "sort-by (created_at desc)"
    (db/clear-query-state!)
    (let [result (->> (q "(and (todo now later done)
                               (sort-by created_at desc))")
                      :result
                      deref
                      (map #(get-in % [:block/properties "created_at"])))]
      (is (= result
             '(1609052959376 1609052958714 1608968448115 1608968448114 1608968448113)))))

  (testing "sort-by (created_at asc)"
    (db/clear-query-state!)
    (let [result (->> (q "(and (todo now later done)
                               (sort-by created_at asc))")
                      :result
                      deref
                      (map #(get-in % [:block/properties "created_at"])))]
      (is (= result
             '(1608968448113 1608968448114 1608968448115 1609052958714 1609052959376)))))

  (testing "sort-by (last_modified_at defaults to desc)"
    (db/clear-query-state!)
    (let [result (->> (q "(and (todo now later done)
                               (sort-by last_modified_at))")
                      :result
                      deref
                      (map #(get-in % [:block/properties "last_modified_at"])))]
      (is (= result
             '(1609052974285 1609052958714 1608968448120 1608968448115 1608968448113)))))

  (testing "sort-by (last_modified_at desc)"
    (db/clear-query-state!)
    (let [result (->> (q "(and (todo now later done)
                               (sort-by last_modified_at desc))")
                      :result
                      deref
                      (map #(get-in % [:block/properties "last_modified_at"])))]
      (is (= result
             '(1609052974285 1609052958714 1608968448120 1608968448115 1608968448113)))))

  (testing "sort-by (last_modified_at desc)"
    (db/clear-query-state!)
    (let [result (->> (q "(and (todo now later done)
                               (sort-by last_modified_at asc))")
                      :result
                      deref
                      (map #(get-in % [:block/properties "last_modified_at"])))]
      (is (= result
             '(1608968448113 1608968448115 1608968448120 1609052958714 1609052974285))))))

(use-fixtures :once
  {:before (fn []
             (config/start-test-db!)
             (import-test-data!))
   :after config/destroy-test-db!})

#_(cljs.test/test-ns 'frontend.db.query-dsl-test)

(comment
  (require '[clojure.pprint :as pprint])
  (config/start-test-db!)
  (import-test-data!)

  (dsl/query test-db "(all-page-tags)")

  ;; (or (priority a) (not (priority a)))
  ;; FIXME: Error: Insufficient bindings: #{?priority} not bound in [(contains? #{"A"} ?priority)]
  (pprint/pprint
   (d/q
    '[:find (pull ?b [*])
      :where
      [?b :block/uuid]
      (or (and [?b :block/priority ?priority] [(contains? #{"A"} ?priority)])
          (not [?b :block/priority #{"A"}]
               [(contains? #{"A"} ?priority)]))]
    (frontend.db/get-conn test-db))))
