Skip to content
Snippets Groups Projects
  • John Swanson's avatar
    25bab869
    Don't dissoc `:id` in `before-update` (#50375) · 25bab869
    John Swanson authored
    * Don't dissoc `:id` in `before-update`
    
    There seems to be a nasty footgun in toucan2 here.
    
    Say you're executing an `update!` command on a set of IDs, e.g.:
    
    ```
    (t2/update! :model/Card :id [:in 1 2] {:view_count 1})
    ```
    
    This works if:
    
    - both cards 1 and 2 have `view_count=1` already
    
    - both cards 1 and 2 have `view_count!=1`
    
    However.
    
    If one of the two cards has `view_count=1` and another has a different
    view_count, then (if the `before-update` method doesn't have the primary
    key attached) Toucan emits a call to update *every card in the
    database*, without a where clause at all.
    Don't dissoc `:id` in `before-update` (#50375)
    John Swanson authored
    * Don't dissoc `:id` in `before-update`
    
    There seems to be a nasty footgun in toucan2 here.
    
    Say you're executing an `update!` command on a set of IDs, e.g.:
    
    ```
    (t2/update! :model/Card :id [:in 1 2] {:view_count 1})
    ```
    
    This works if:
    
    - both cards 1 and 2 have `view_count=1` already
    
    - both cards 1 and 2 have `view_count!=1`
    
    However.
    
    If one of the two cards has `view_count=1` and another has a different
    view_count, then (if the `before-update` method doesn't have the primary
    key attached) Toucan emits a call to update *every card in the
    database*, without a where clause at all.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
card_test.clj 61.37 KiB
(ns metabase.models.card-test
  (:require
   [cheshire.core :as json]
   [clojure.set :as set]
   [clojure.test :refer :all]
   [java-time.api :as t]
   [metabase.audit :as audit]
   [metabase.config :as config]
   [metabase.lib.core :as lib]
   [metabase.lib.metadata :as lib.metadata]
   [metabase.lib.metadata.jvm :as lib.metadata.jvm]
   [metabase.models.card :as card]
   [metabase.models.interface :as mi]
   [metabase.models.parameter-card :as parameter-card]
   [metabase.models.revision :as revision]
   [metabase.models.serialization :as serdes]
   [metabase.query-processor.card-test :as qp.card-test]
   [metabase.query-processor.preprocess :as qp.preprocess]
   [metabase.test :as mt]
   [metabase.test.util :as tu]
   [metabase.util :as u]
   [toucan2.core :as t2]
   [toucan2.tools.with-temp :as t2.with-temp]))

(set! *warn-on-reflection* true)

(deftest dashboard-count-test
  (testing "Check that the :dashboard_count delay returns the correct count of Dashboards a Card is in"
    (t2.with-temp/with-temp [:model/Card      {card-id :id} {}
                             :model/Dashboard dash-1        {}
                             :model/Dashboard dash-2        {}]
      (letfn [(add-card-to-dash! [dash]
                (t2/insert! :model/DashboardCard
                            {:card_id      card-id
                             :dashboard_id (u/the-id dash)
                             :row          0
                             :col          0
                             :size_x       4
                             :size_y       4}))
              (get-dashboard-count []
                (-> (t2/select-one :model/Card :id card-id)
                    (t2/hydrate :dashboard_count)
                    :dashboard_count))]
        (is (= 0
               (get-dashboard-count)))
        (testing "add to a Dashboard"
          (add-card-to-dash! dash-1)
          (is (= 1
                 (get-dashboard-count))))
        (testing "add to a second Dashboard"
          (add-card-to-dash! dash-2)
          (is (= 2
                 (get-dashboard-count))))))))

(deftest dropdown-widget-values-usage-count-test
  (let [hydrated-count (fn [card] (-> card
                                      (t2/hydrate :parameter_usage_count)
                                      :parameter_usage_count))
        default-params {:name       "Category Name"
                        :slug       "category_name"
                        :id         "_CATEGORY_NAME_"
                        :type       "category"}
        card-params    (fn [card-id] (merge default-params {:values_source_type "card"
                                                            :values_source_config {:card_id card-id}}))]
    (testing "With no associated cards"
      (t2.with-temp/with-temp [:model/Card card]
        (is (zero? (hydrated-count card)))))
    (testing "With one"
      (t2.with-temp/with-temp [:model/Card      {card-id :id :as card} {}
                               :model/Dashboard _                      {:parameters [(card-params card-id)]}]
        (is (= 1 (hydrated-count card)))))
    (testing "With several"
      (t2.with-temp/with-temp [:model/Card      {card-id :id :as card} {}
                               :model/Dashboard _                      {:parameters [(card-params card-id)]}
                               :model/Dashboard _                      {:parameters [(card-params card-id)]}
                               :model/Dashboard _                      {:parameters [(card-params card-id)]}]
        (is (= 3 (hydrated-count card)))))))

(deftest public-sharing-test
  (testing "test that a Card's :public_uuid comes back if public sharing is enabled..."
    (tu/with-temporary-setting-values [enable-public-sharing true]
      (t2.with-temp/with-temp [:model/Card card {:public_uuid (str (random-uuid))}]
        (is (=? u/uuid-regex
                (:public_uuid card)))))))

(deftest public-sharing-test-2
  (testing "test that a Card's :public_uuid comes back if public sharing is enabled..."
    (testing "...but if public sharing is *disabled* it should come back as `nil`"
      (tu/with-temporary-setting-values [enable-public-sharing false]
        (t2.with-temp/with-temp [:model/Card card {:public_uuid (str (random-uuid))}]
          (is (= nil
                 (:public_uuid card))))))))

(defn- dummy-dataset-query [database-id]
  {:database database-id
   :type     :native
   :native   {:query "SELECT count(*) FROM toucan_sightings;"}})

(deftest database-id-test
  (t2.with-temp/with-temp [:model/Card {:keys [id]} {:name          "some name"
                                                     :dataset_query (dummy-dataset-query (mt/id))
                                                     :database_id   (mt/id)}]
    (testing "before update"
      (is (= {:name "some name", :database_id (mt/id)}
             (into {} (t2/select-one [:model/Card :name :database_id] :id id)))))
    (t2/update! :model/Card id {:name          "another name"
                                :dataset_query (dummy-dataset-query (mt/id))})
    (testing "after update"
      (is (= {:name "another name" :database_id (mt/id)}
             (into {} (t2/select-one [:model/Card :name :database_id] :id id)))))))

(deftest disable-implicit-actions-if-needed-test
  (mt/with-actions-enabled
    (testing "when updating a model to include any clauses will disable implicit actions if they exist\n"
      (testing "happy paths\n"
        (let [query (mt/mbql-query users)]
          (doseq [query-change [{:limit       1}
                                {:expressions {"id + 1" [:+ (mt/$ids $users.id) 1]}}
                                {:filter      [:> (mt/$ids $users.id) 2]}
                                {:breakout    [(mt/$ids !month.users.last_login)]}
                                {:aggregation [[:count]]}
                                {:joins       [{:fields       :all
                                                :source-table (mt/id :checkins)
                                                :condition    [:= (mt/$ids $users.id) (mt/$ids $checkins.user_id)]
                                                :alias        "People"}]}
                                {:order-by    [[(mt/$ids $users.id) :asc]]}
                                {:fields      [(mt/$ids $users.id)]}]]
            (testing (format "when adding %s to the query" (first (keys query-change)))
              (mt/with-actions [{model-id :id}           {:type :model, :dataset_query query}
                                {action-id-1 :action-id} {:type :implicit
                                                          :kind "row/create"}
                                {action-id-2 :action-id} {:type :implicit
                                                          :kind "row/update"}]
                ;; make sure we have thing exists to start with
                (is (= 2 (t2/count :model/Action :id [:in [action-id-1 action-id-2]])))
                (is (= 1 (t2/update! :model/Card :id model-id {:dataset_query (update query :query merge query-change)})))
                ;; should be gone by now
                (is (= 0 (t2/count :model/Action :id [:in [action-id-1 action-id-2]])))
                (is (= 0 (t2/count :model/ImplicitAction :action_id [:in [action-id-1 action-id-2]])))
                ;; call it twice to make we don't get delete error if no actions are found
                (is (= 1 (t2/update! :model/Card :id model-id {:dataset_query (update query :query merge query-change)})))))))))))

(deftest disable-implicit-actions-if-needed-test-2
  (mt/with-actions-enabled
    (testing "unhappy paths\n"
      (testing "should not attempt to delete if it's not a model"
        (t2.with-temp/with-temp [:model/Card {id :id} {:type          :question
                                                       :dataset_query (mt/mbql-query users)}]
          (with-redefs [card/disable-implicit-action-for-model! (fn [& _args]
                                                                  (throw (ex-info "Should not be called" {})))]
            (is (= 1 (t2/update! :model/Card :id id {:dataset_query (mt/mbql-query users {:limit 1})})))))))))

(deftest disable-implicit-actions-if-needed-test-3
  (mt/with-actions-enabled
    (testing "unhappy paths\n"
      (testing "only disable implicit actions, not http and query"
        (mt/with-actions [{model-id :id}           {:type :model, :dataset_query (mt/mbql-query users)}
                          {implicit-id :action-id} {:type :implicit}
                          {http-id :action-id}     {:type :http}
                          {query-id :action-id}    {:type :query}]
          ;; make sure we have thing exists to start with
          (is (= 3 (t2/count :model/Action :id [:in [implicit-id http-id query-id]])))
          (t2/update! :model/Card :id model-id {:dataset_query (mt/mbql-query users {:limit 1})})
          (is (not (t2/exists? :model/Action :id implicit-id)))
          (is (t2/exists? :model/Action :id http-id))
          (is (t2/exists? :model/Action :id query-id)))))))

(deftest disable-implicit-actions-if-needed-test-4
  (mt/with-actions-enabled
    (testing "unhappy paths\n"
      (testing "should not disable if change source table"
        (mt/with-actions [{model-id :id}           {:type :model, :dataset_query (mt/mbql-query users)}
                          {action-id-1 :action-id} {:type :implicit
                                                    :kind "row/create"}
                          {action-id-2 :action-id} {:type :implicit
                                                    :kind "row/update"}]
          ;; make sure we have thing exists to start with
          (is (= 2 (t2/count :model/Action :id [:in [action-id-1 action-id-2]])))
          ;; change source from users to categories
          (t2/update! :model/Card :id model-id {:dataset_query (mt/mbql-query categories)})
          ;; actions still exists
          (is (= 2 (t2/count :model/Action :id [:in [action-id-1 action-id-2]])))
          (is (= 2 (t2/count :model/ImplicitAction :action_id [:in [action-id-1 action-id-2]]))))))))

;;; ------------------------------------------ Circular Reference Detection ------------------------------------------

(defn- card-with-source-table
  "Generate values for a Card with `source-table` for use with `with-temp`."
  [source-table & {:as kvs}]
  (merge {:dataset_query {:database (mt/id)
                          :type     :query
                          :query    {:source-table source-table}}}
         kvs))

(deftest circular-reference-test
  (testing "Should throw an Exception if saving a Card that references itself"
    (t2.with-temp/with-temp [:model/Card card (card-with-source-table (mt/id :venues))]
      ;; now try to make the Card reference itself. Should throw Exception
      (is (thrown?
           Exception
           (t2/update! :model/Card (u/the-id card)
                       (card-with-source-table (str "card__" (u/the-id card)))))))))

(deftest circular-reference-test-2
  (testing "Do the same stuff with circular reference between two Cards... (A -> B -> A)"
    (t2.with-temp/with-temp [:model/Card card-a (card-with-source-table (mt/id :venues))
                             :model/Card card-b (card-with-source-table (str "card__" (u/the-id card-a)))]
      (is (thrown?
           Exception
           (t2/update! :model/Card (u/the-id card-a)
                       (card-with-source-table (str "card__" (u/the-id card-b)))))))))

(deftest circular-reference-test-3
  (testing "ok now try it with A -> C -> B -> A"
    (t2.with-temp/with-temp [:model/Card card-a (card-with-source-table (mt/id :venues))
                             :model/Card card-b (card-with-source-table (str "card__" (u/the-id card-a)))
                             :model/Card card-c (card-with-source-table (str "card__" (u/the-id card-b)))]
      (is (thrown?
           Exception
           (t2/update! :model/Card (u/the-id card-a)
                       (card-with-source-table (str "card__" (u/the-id card-c)))))))))

(deftest validate-collection-namespace-test
  (t2.with-temp/with-temp [:model/Collection {collection-id :id} {:namespace "currency"}]
    (testing "Shouldn't be able to create a Card in a non-normal Collection"
      (let [card-name (mt/random-name)]
        (try
          (is (thrown-with-msg?
               clojure.lang.ExceptionInfo
               #"A Card can only go in Collections in the \"default\" or :analytics namespace."
               (t2/insert! :model/Card (assoc (t2.with-temp/with-temp-defaults :model/Card) :collection_id collection-id, :name card-name))))
          (finally
            (t2/delete! :model/Card :name card-name)))))))

(deftest validate-collection-namespace-test-2
  (t2.with-temp/with-temp [:model/Collection {collection-id :id} {:namespace "currency"}]
    (testing "Shouldn't be able to move a Card to a non-normal Collection"
      (t2.with-temp/with-temp [:model/Card {card-id :id}]
        (is (thrown-with-msg?
             clojure.lang.ExceptionInfo
             #"A Card can only go in Collections in the \"default\" or :analytics namespace."
             (t2/update! :model/Card card-id {:collection_id collection-id})))))))

(deftest ^:parallel normalize-result-metadata-test
  (testing "Should normalize result metadata keys when fetching a Card from the DB"
    (let [metadata (qp.preprocess/query->expected-cols (mt/mbql-query venues))]
      (t2.with-temp/with-temp [:model/Card {card-id :id} {:dataset_query   (mt/mbql-query venues)
                                                          :result_metadata metadata}]
        (is (= (mt/derecordize metadata)
               (mt/derecordize (t2/select-one-fn :result_metadata :model/Card :id card-id))))))))

(deftest populate-result-metadata-if-needed-test
  (doseq [[creating-or-updating f]
          {"creating" (fn [properties f]
                        (t2.with-temp/with-temp [:model/Card {card-id :id} properties]
                          (f (t2/select-one-fn :result_metadata :model/Card :id card-id))))
           "updating" (fn [changes f]
                        (t2.with-temp/with-temp [:model/Card {card-id :id} {:dataset_query   (mt/mbql-query checkins)
                                                                            :result_metadata (qp.preprocess/query->expected-cols (mt/mbql-query checkins))}]
                          (t2/update! :model/Card card-id changes)
                          (f (t2/select-one-fn :result_metadata :model/Card :id card-id))))}]

    (testing (format "When %s a Card\n" creating-or-updating)
      (testing "If result_metadata is empty, we should attempt to populate it"
        (f {:dataset_query (mt/mbql-query venues)}
           (fn [metadata]
             (is (= (map :name (qp.preprocess/query->expected-cols (mt/mbql-query venues)))
                    (map :name metadata))))))
      (testing "Don't overwrite result_metadata that was passed in"
        (let [metadata (take 1 (qp.preprocess/query->expected-cols (mt/mbql-query venues)))]
          (f {:dataset_query   (mt/mbql-query venues)
              :result_metadata metadata}
             (fn [new-metadata]
               (is (= (mt/derecordize metadata)
                      (mt/derecordize new-metadata)))))))
      (testing "Shouldn't barf if query can't be run (e.g. if query is a SQL query); set metadata to nil"
        (f {:dataset_query (mt/native-query {:native "SELECT * FROM VENUES"})}
           (fn [metadata]
             (is (= nil
                    metadata)))))
      (testing "Shouldn't remove verified result metadata from native queries (#37009)"
        (let [metadata (qp.preprocess/query->expected-cols (mt/mbql-query checkins))]
          (f (cond-> {:dataset_query (mt/native-query {:native "SELECT * FROM CHECKINS"})
                      :result_metadata metadata}
               (= creating-or-updating "updating")
               (assoc :verified-result-metadata? true))
             (fn [new-metadata]
               (is (= (mt/derecordize metadata)
                      (mt/derecordize new-metadata))))))))))

(defn- test-visualization-settings-normalization-1 [f]
  (testing "visualization settings should get normalized to use modern MBQL syntax"
    (testing "Field references in column settings"
      (doseq [[original expected] {[:ref [:field-literal "foo" :type/Float]]
                                   [:ref [:field "foo" {:base-type :type/Float}]]

                                   [:ref [:field-id 1]]
                                   [:ref [:field 1 nil]]

                                   [:ref [:expression "wow"]]
                                   [:ref [:expression "wow"]]}
              ;; also check that normalization of already-normalized refs is idempotent
              original [original expected]
              ;; frontend uses JSON-serialized versions of the MBQL clauses as keys
              :let     [original (json/generate-string original)
                        expected (json/generate-string expected)]]
        (testing (format "Viz settings field ref key %s should get normalized to %s"
                         (pr-str original)
                         (pr-str expected))
          (f
           {:column_settings {original {:currency "BTC"}}}
           {:column_settings {expected {:currency "BTC"}}}))))))

(defn- test-visualization-settings-normalization-2 [f]
  (testing "visualization settings should get normalized to use modern MBQL syntax"
    (testing "Other MBQL field clauses"
      (let [original {:map.type                 "region"
                      :map.region               "us_states"
                      :pivot_table.column_split {:rows    [["datetime-field" ["field-id" 807] "year"]]
                                                 :columns [["fk->" ["field-id" 805] ["field-id" 808]]]
                                                 :values  [["aggregation" 0]]}}
            expected {:map.type                 "region"
                      :map.region               "us_states"
                      :pivot_table.column_split {:rows    [[:field 807 {:temporal-unit :year}]]
                                                 :columns [[:field 808 {:source-field 805}]]
                                                 :values  [[:aggregation 0]]}}]
        (f original expected)))))

(defn- test-visualization-settings-normalization-3 [f]
  (testing "visualization settings should get normalized to use modern MBQL syntax"
    (testing "Don't normalize non-MBQL arrays"
      (let [original {:graph.show_goal  true
                      :graph.goal_value 5.9
                      :graph.dimensions ["the_day"]
                      :graph.metrics    ["total_per_day"]}]
        (f original original)))))

(defn- test-visualization-settings-normalization-4 [f]
  (testing "visualization settings should get normalized to use modern MBQL syntax"
    (testing "Don't normalize key-value pairs in maps that could be interpreted as MBQL clauses"
      (let [original {:field-id 1}]
        (f original original)))))

(defn- test-visualization-settings-normalization-5 [f]
  (testing "visualization settings should get normalized to use modern MBQL syntax"
    (testing "Don't normalize array in graph.metrics that could be interpreted as MBQL clauses"
      (let [original {:graph.metrics ["expression" "sum" "count"]}]
        (f original original)))))

;; this is a separate function so we can use the same tests for DashboardCards as well
(defn test-visualization-settings-normalization [f]
  (testing "visualization settings should get normalized to use modern MBQL syntax"
    (doseq [varr [#'test-visualization-settings-normalization-1
                  #'test-visualization-settings-normalization-2
                  #'test-visualization-settings-normalization-3
                  #'test-visualization-settings-normalization-4
                  #'test-visualization-settings-normalization-5]]
      (testing varr
        (varr f)))))

(deftest normalize-visualization-settings-test
  (test-visualization-settings-normalization
   (fn [original expected]
     (t2.with-temp/with-temp [:model/Card card {:visualization_settings original}]
       (is (= expected
              (t2/select-one-fn :visualization_settings :model/Card :id (u/the-id card))))))))

(deftest ^:parallel template-tag-parameters-test
  (testing "Card with a Field filter parameter"
    (mt/with-temp [:model/Card card {:dataset_query (qp.card-test/field-filter-query)}]
      (is (= [{:id "_DATE_"
               :type :date/all-options
               :target [:dimension [:template-tag "date"]]
               :name "Check-In Date"
               :slug "date"
               :default nil
               :required false}]
             (card/template-tag-parameters card))))))

(deftest ^:parallel template-tag-parameters-test-2
  (testing "Card with a non-Field-filter parameter"
    (mt/with-temp [:model/Card card {:dataset_query (qp.card-test/non-field-filter-query)}]
      (is (= [{:id "_ID_"
               :type :number/=
               :target [:variable [:template-tag "id"]]
               :name "Order ID"
               :slug "id"
               :default "1"
               :required true}]
             (card/template-tag-parameters card))))))

(deftest ^:parallel template-tag-parameters-test-3
  (testing "Should ignore native query snippets and source card IDs"
    (mt/with-temp [:model/Card card {:dataset_query (qp.card-test/non-parameter-template-tag-query)}]
      (is (= [{:id "_ID_"
               :type :number/=
               :target [:variable [:template-tag "id"]]
               :name "Order ID"
               :slug "id"
               :default "1"
               :required true}]
             (card/template-tag-parameters card))))))

(deftest validate-template-tag-field-ids-test
  (testing "Disallow saving a Card with native query Field filter template tags referencing a different Database (#14145)"
    (let [test-data-db-id   (mt/id)
          bird-counts-db-id (mt/dataset daily-bird-counts (mt/id))
          card-data         (fn [database-id]
                              {:database_id   database-id
                               :dataset_query {:database database-id
                                               :type     :native
                                               :native   {:query         "SELECT COUNT(*) FROM PRODUCTS WHERE {{FILTER}}"
                                                          :template-tags {"FILTER" {:id           "_FILTER_"
                                                                                    :name         "FILTER"
                                                                                    :display-name "Filter"
                                                                                    :type         :dimension
                                                                                    :dimension    [:field (mt/id :venues :name) nil]
                                                                                    :widget-type  :string/=
                                                                                    :default      nil}}}}})
          good-card-data  (card-data test-data-db-id)
          bad-card-data   (card-data bird-counts-db-id)]
      (testing "Should not be able to create new Card with a filter with the wrong Database ID"
        (is (thrown-with-msg?
             clojure.lang.ExceptionInfo
             #"Invalid Field Filter: Field \d+ \"VENUES\"\.\"NAME\" belongs to Database \d+ \"test-data \(h2\)\", but the query is against Database \d+ \"daily-bird-counts \(h2\)\""
             (t2.with-temp/with-temp [:model/Card _ bad-card-data]))))
      (testing "Should not be able to update a Card to have a filter with the wrong Database ID"
        (t2.with-temp/with-temp [:model/Card {card-id :id} good-card-data]
          (is (thrown-with-msg?
               clojure.lang.ExceptionInfo
               #"Invalid Field Filter: Field \d+ \"VENUES\"\.\"NAME\" belongs to Database \d+ \"test-data \(h2\)\", but the query is against Database \d+ \"daily-bird-counts \(h2\)\""
               (t2/update! :model/Card card-id bad-card-data))))))))

;;; ------------------------------------------ Parameters tests ------------------------------------------

(deftest ^:parallel validate-parameters-test
  (testing "Should validate Card :parameters when"
    (testing "creating"
      (is (thrown-with-msg?
           clojure.lang.ExceptionInfo
           #":parameters must be a sequence of maps with :id and :type keys"
           (t2.with-temp/with-temp [:model/Card _ {:parameters {:a :b}}])))
      (t2.with-temp/with-temp [:model/Card card {:parameters [{:id   "valid-id"
                                                               :type "id"}]}]
        (is (some? card))))))

(deftest validate-parameters-test-2
  (testing "Should validate Card :parameters when"
    (testing "updating"
      (t2.with-temp/with-temp [:model/Card {:keys [id]} {:parameters []}]
        (is (thrown-with-msg?
             clojure.lang.ExceptionInfo
             #":parameters must be a sequence of maps with :id and :type keys"
             (t2/update! :model/Card id {:parameters [{:id 100}]})))
        (is (pos? (t2/update! :model/Card id {:parameters [{:id   "new-valid-id"
                                                            :type "id"}]})))))))

(deftest normalize-parameters-test
  (testing ":parameters should get normalized when coming out of the DB"
    (doseq [[target expected] {[:dimension [:field-id 1000]] [:dimension [:field 1000 nil]]
                               [:field-id 1000]              [:field 1000 nil]}]
      (testing (format "target = %s" (pr-str target))
        (t2.with-temp/with-temp [:model/Card {card-id :id} {:parameter_mappings [{:parameter_id     "_CATEGORY_NAME_"
                                                                                  :target target}]}]

          (is (= [{:parameter_id     "_CATEGORY_NAME_"
                   :target expected}]
                 (t2/select-one-fn :parameter_mappings :model/Card :id card-id))))))))

(deftest validate-parameter-mappings-test
  (testing "Should validate Card :parameter_mappings when"
    (testing "creating"
      (is (thrown-with-msg?
           clojure.lang.ExceptionInfo
           #":parameter_mappings must be a sequence of maps with :parameter_id and :type keys"
           (t2.with-temp/with-temp [:model/Card _ {:parameter_mappings {:a :b}}])))
      (t2.with-temp/with-temp [:model/Card card {:parameter_mappings [{:parameter_id "valid-id"
                                                                       :target       [:field 1000 nil]}]}]
        (is (some? card))))))

(deftest validate-parameter-mappings-test-2
  (testing "Should validate Card :parameter_mappings when"
    (testing "updating"
      (t2.with-temp/with-temp [:model/Card {:keys [id]} {:parameter_mappings []}]
        (is (thrown-with-msg?
             clojure.lang.ExceptionInfo
             #":parameter_mappings must be a sequence of maps with :parameter_id and :type keys"
             (t2/update! :model/Card id {:parameter_mappings [{:parameter_id 100}]})))
        (is (pos? (t2/update! :model/Card id {:parameter_mappings [{:parameter_id "new-valid-id"
                                                                    :target       [:field 1000 nil]}]})))))))

(deftest normalize-parameter-mappings-test
  (testing ":parameter_mappings should get normalized when coming out of the DB"
    (t2.with-temp/with-temp [:model/Card {card-id :id} {:parameter_mappings [{:parameter_id "22486e00"
                                                                              :card_id      1
                                                                              :target       [:dimension [:field-id 1]]}]}]
      (is (= [{:parameter_id "22486e00"
               :card_id      1
               :target       [:dimension [:field 1 nil]]}]
             (t2/select-one-fn :parameter_mappings :model/Card :id card-id))))))

(deftest identity-hash-test
  (testing "Card hashes are composed of the name and the collection's hash"
    (let [now #t "2022-09-01T12:34:56"]
      (mt/with-temp [:model/Collection  coll {:name "field-db" :location "/" :created_at now}
                     :model/Card card {:name "the card" :collection_id (:id coll) :created_at now}]
        (is (= "5199edf0"
               (serdes/raw-hash ["the card" (serdes/identity-hash coll) now])
               (serdes/identity-hash card)))))))

(deftest parameter-card-test
  (let [default-params {:name       "Category Name"
                        :slug       "category_name"
                        :id         "_CATEGORY_NAME_"
                        :type       "category"}]
    (testing "parameter with source is card create ParameterCard"
      (t2.with-temp/with-temp [:model/Card  {source-card-id-1 :id} {}
                               :model/Card  {source-card-id-2 :id} {}
                               :model/Card  {card-id :id}          {:parameters [(merge default-params
                                                                                        {:values_source_type    "card"
                                                                                         :values_source_config {:card_id source-card-id-1}})]}]
        (is (=? [{:card_id                   source-card-id-1
                  :parameterized_object_type :card
                  :parameterized_object_id   card-id
                  :parameter_id              "_CATEGORY_NAME_"}]
                (t2/select :model/ParameterCard :parameterized_object_type "card" :parameterized_object_id card-id)))

        (testing "update values_source_config.card_id will update ParameterCard"
          (t2/update! :model/Card card-id {:parameters [(merge default-params
                                                               {:values_source_type    "card"
                                                                :values_source_config {:card_id source-card-id-2}})]})
          (is (=? [{:card_id                   source-card-id-2
                    :parameterized_object_type :card
                    :parameterized_object_id   card-id
                    :parameter_id              "_CATEGORY_NAME_"}]
                  (t2/select :model/ParameterCard :parameterized_object_type "card" :parameterized_object_id card-id))))

        (testing "delete the card will delete ParameterCard"
          (t2/delete! :model/Card :id card-id)
          (is (= []
                 (t2/select :model/ParameterCard :parameterized_object_type "card" :parameterized_object_id card-id))))))))

(deftest parameter-card-test-2
  (let [default-params {:name       "Category Name"
                        :slug       "category_name"
                        :id         "_CATEGORY_NAME_"
                        :type       "category"}]
    (testing "Delete a card will delete any ParameterCard that linked to it"
      (t2.with-temp/with-temp [:model/Card  {source-card-id :id} {}
                               :model/Card  {card-id-1 :id}      {:parameters [(merge default-params
                                                                                      {:values_source_type    "card"
                                                                                       :values_source_config {:card_id source-card-id}})]}
                               :model/Card  {card-id-2 :id}      {:parameters [(merge default-params
                                                                                      {:values_source_type    "card"
                                                                                       :values_source_config {:card_id source-card-id}})]}]
        ;; makes sure we have ParameterCard to start with
        (is (=? [{:card_id                   source-card-id
                  :parameterized_object_type :card
                  :parameterized_object_id   card-id-1
                  :parameter_id              "_CATEGORY_NAME_"}
                 {:card_id                   source-card-id
                  :parameterized_object_type :card
                  :parameterized_object_id   card-id-2
                  :parameter_id              "_CATEGORY_NAME_"}]
                (t2/select :model/ParameterCard :card_id source-card-id {:order-by [[:parameterized_object_id :asc]]})))
        (t2/delete! :model/Card :id source-card-id)
        (is (= []
               (t2/select :model/ParameterCard :card_id source-card-id)))))))

(deftest do-not-update-parameter-card-if-it-doesn't-change-test
  (testing "Do not update ParameterCard if updating a Dashboard doesn't change the parameters"
    (mt/with-temp [:model/Card  {source-card-id :id} {}
                   :model/Card  {card-id-1 :id}      {:parameters [{:name       "Category Name"
                                                                    :slug       "category_name"
                                                                    :id         "_CATEGORY_NAME_"
                                                                    :type       "category"
                                                                    :values_source_type    "card"
                                                                    :values_source_config {:card_id source-card-id}}]}]
      (mt/with-dynamic-redefs [parameter-card/upsert-or-delete-from-parameters! (fn [& _] (throw (ex-info "Should not be called" {})))]
        (t2/update! :model/Card card-id-1 {:name "new name"})))))

(deftest cleanup-parameter-on-card-changes-test
  (mt/dataset test-data
    (mt/with-temp
      [:model/Card        {source-card-id :id} (merge (mt/card-with-source-metadata-for-query
                                                       (mt/mbql-query products {:fields [(mt/$ids $products.title)
                                                                                         (mt/$ids $products.category)]
                                                                                :limit 5}))
                                                      {:database_id (mt/id)
                                                       :table_id    (mt/id :products)})
       :model/Card        card                 {:parameters [{:name                  "Param 1"
                                                              :id                    "param_1"
                                                              :type                  "category"
                                                              :values_source_type    "card"
                                                              :values_source_config {:card_id source-card-id
                                                                                     :value_field (mt/$ids $products.title)}}]}
       :model/Dashboard   dashboard            {:parameters [{:name       "Param 2"
                                                              :id         "param_2"
                                                              :type       "category"
                                                              :values_source_type    "card"
                                                              :values_source_config {:card_id source-card-id
                                                                                     :value_field (mt/$ids $products.category)}}]}]
      ;; check if we had parametercard to starts with
      (is (=? [{:card_id                   source-card-id
                :parameter_id              "param_1"
                :parameterized_object_type :card
                :parameterized_object_id   (:id card)}
               {:card_id                   source-card-id
                :parameter_id              "param_2"
                :parameterized_object_type :dashboard
                :parameterized_object_id   (:id dashboard)}]
              (t2/select :model/ParameterCard :card_id source-card-id {:order-by [[:parameter_id :asc]]})))
      ;; update card with removing the products.category
      (testing "on update result_metadata"
        (t2/update! :model/Card source-card-id
                    (mt/card-with-source-metadata-for-query
                     (mt/mbql-query products {:fields [(mt/$ids $products.title)]
                                              :limit 5})))

        (testing "ParameterCard for dashboard is removed"
          (is (=? [{:card_id                   source-card-id
                    :parameter_id              "param_1"
                    :parameterized_object_type :card
                    :parameterized_object_id   (:id card)}]
                  (t2/select :model/ParameterCard :card_id source-card-id))))

        (testing "update the dashboard parameter and remove values_config of dashboard"
          (is (=? [{:id   "param_2"
                    :name "Param 2"
                    :type :category}]
                  (t2/select-one-fn :parameters :model/Dashboard :id (:id dashboard))))

          (testing "but no changes with parameter on card"
            (is (=? [{:name                 "Param 1"
                      :id                   "param_1"
                      :type                 :category
                      :values_source_type   "card"
                      :values_source_config {:card_id     source-card-id
                                             :value_field (mt/$ids $products.title)}}]
                    (t2/select-one-fn :parameters :model/Card :id (:id card)))))))

      (testing "on archive card"
        (t2/update! :model/Card source-card-id {:archived true})

        (testing "ParameterCard for card is removed"
          (is (=? [] (t2/select :model/ParameterCard :card_id source-card-id))))

        (testing "update the dashboard parameter and remove values_config of card"
          (is (=? [{:id   "param_1"
                    :name "Param 1"
                    :type :category}]
                  (t2/select-one-fn :parameters :model/Card :id (:id card)))))))))

(deftest ^:parallel descendants-test
  (testing "regular cards don't depend on anything"
    (mt/with-temp [:model/Card card {:name "some card"}]
      (is (empty? (serdes/descendants "Card" (:id card)))))))

(deftest ^:parallel descendants-test-2
  (testing "cards which have another card as the source depend on that card"
    (mt/with-temp [:model/Card card1 {:name "base card"}
                   :model/Card card2 {:name "derived card"
                                      :dataset_query {:query {:source-table (str "card__" (:id card1))}}}]
      (is (empty? (serdes/descendants "Card" (:id card1))))
      (is (= {["Card" (:id card1)] {"Card" (:id card2)}}
             (serdes/descendants "Card" (:id card2)))))))

(deftest ^:parallel descendants-test-3
  (testing "cards that has a native template tag"
    (mt/with-temp [:model/NativeQuerySnippet snippet {:name "category" :content "category = 'Gizmo'"}
                   :model/Card               card
                   {:name          "Business Card"
                    :dataset_query {:native
                                    {:template-tags {:snippet {:name         "snippet"
                                                               :type         :snippet
                                                               :snippet-name "snippet"
                                                               :snippet-id   (:id snippet)}}
                                     :query "select * from products where {{snippet}}"}}}]
      (is (= {["NativeQuerySnippet" (:id snippet)] {"Card" (:id card)}}
             (serdes/descendants "Card" (:id card)))))))

(deftest ^:parallel descendants-test-4
  (testing "cards which have parameter's source is another card"
    (mt/with-temp [:model/Card card1 {:name "base card"}
                   :model/Card card2 {:name       "derived card"
                                      :parameters [{:id                   "valid-id"
                                                    :type                 "id"
                                                    :values_source_type   "card"
                                                    :values_source_config {:card_id (:id card1)}}]}]
      (is (= {["Card" (:id card1)] {"Card" (:id card2)}}
             (serdes/descendants "Card" (:id card2)))))))

(deftest ^:parallel extract-test
  (let [metadata (qp.preprocess/query->expected-cols (mt/mbql-query venues))
        query    (mt/mbql-query venues)]
    (testing "every card retains result_metadata"
      (t2.with-temp/with-temp [:model/Card {card1-id :id} {:dataset_query   query
                                                           :result_metadata metadata}
                               :model/Card {card2-id :id} {:type            :model
                                                           :dataset_query   query
                                                           :result_metadata metadata}]
        (doseq [card-id [card1-id card2-id]]
          (let [extracted (serdes/extract-one "Card" nil (t2/select-one :model/Card :id card-id))]
            ;; card2 is model, but card1 is not
            (is (= (= card-id card2-id)
                   (= :model (:type extracted))))
            (is (string? (:display_name (first (:result_metadata extracted)))))
            ;; this is a quick comparison, since the actual stored metadata is quite complex
            (is (= (map :display_name metadata)
                   (map :display_name (:result_metadata extracted))))))))))

;;; ------------------------------------------ Viz Settings Tests  ------------------------------------------

(deftest ^:parallel upgrade-to-v2-db-test
  (testing ":visualization_settings v. 1 should be upgraded to v. 2 on select"
    (t2.with-temp/with-temp [:model/Card {card-id :id} {:visualization_settings {:pie.show_legend true}}]
      (is (= {:version 2
              :pie.show_legend true
              :pie.percent_visibility "inside"}
             (t2/select-one-fn :visualization_settings :model/Card :id card-id))))))

(deftest upgrade-to-v2-db-test-2
  (testing ":visualization_settings v. 1 should be upgraded to v. 2 and persisted on update"
    (t2.with-temp/with-temp [:model/Card {card-id :id} {:visualization_settings {:pie.show_legend true}}]
      (t2/update! :model/Card card-id {:name "Favorite Toucan Foods"})
      (is (= {:version 2
              :pie.show_legend true
              :pie.percent_visibility "inside"}
             (-> (t2/select-one (t2/table-name :model/Card) {:where [:= :id card-id]})
                 :visualization_settings
                 (json/parse-string keyword)))))))

;;; -------------------------------------------- Revision tests  --------------------------------------------

(deftest ^:parallel diff-cards-str-test
  (are [x y expected] (= expected
                         (u/build-sentence (revision/diff-strings :model/Card x y)))
    {:name        "Diff Test"
     :description nil}
    {:name        "Diff Test Changed"
     :description "foobar"}
    "added a description and renamed it from \"Diff Test\" to \"Diff Test Changed\"."

    {:name "Apple"}
    {:name "Next"}
    "renamed this Card from \"Apple\" to \"Next\"."

    {:display :table}
    {:display :pie}
    "changed the display from table to pie."

    {:name        "Diff Test"
     :description nil}
    {:name        "Diff Test changed"
     :description "New description"}
    "added a description and renamed it from \"Diff Test\" to \"Diff Test changed\"."))

(deftest ^:parallel diff-cards-str-update-collection--test
  (t2.with-temp/with-temp
    [:model/Collection {coll-id-1 :id} {:name "Old collection"}
     :model/Collection {coll-id-2 :id} {:name "New collection"}]
    (are [x y expected] (= expected
                           (u/build-sentence (revision/diff-strings :model/Card x y)))

      {:name "Apple"}
      {:name          "Apple"
       :collection_id coll-id-2}
      "moved this Card to New collection."

      {:name        "Diff Test"
       :description nil}
      {:name        "Diff Test changed"
       :description "New description"}
      "added a description and renamed it from \"Diff Test\" to \"Diff Test changed\"."

      {:name          "Apple"
       :collection_id coll-id-1}
      {:name          "Apple"
       :collection_id coll-id-2}
      "moved this Card from Old collection to New collection.")))

(defn- create-card-revision!
  "Fetch the latest version of a Dashboard and save a revision entry for it. Returns the fetched Dashboard."
  [card-id is-creation?]
  (revision/push-revision!
   {:object       (t2/select-one :model/Card :id card-id)
    :entity       :model/Card
    :id           card-id
    :user-id      (mt/user->id :crowberto)
    :is-creation? is-creation?}))

(deftest record-revision-and-description-completeness-test
  (t2.with-temp/with-temp
    [:model/Database   db   {:name "random db"}
     :model/Card       base-card {}
     :model/Card       card {:name                "A Card"
                             :description         "An important card"
                             :collection_position 0
                             :cache_ttl           1000
                             :archived            false
                             :parameters          [{:name       "Category Name"
                                                    :slug       "category_name"
                                                    :id         "_CATEGORY_NAME_"
                                                    :type       "category"}]}
     :model/Collection coll {:name "A collection"}]
    (mt/with-temporary-setting-values [enable-public-sharing true]
      (let [columns     (disj (set/difference (set (keys card)) (set @#'card/excluded-columns-for-card-revision))
                              ;; we only record result metadata for models, so we'll test that seperately
                              :result_metadata)
            update-col  (fn [col value]
                          (cond
                            (= col :collection_id)     (:id coll)
                            (= col :parameters)        (cons {:name "Category ID"
                                                              :slug "category_id"
                                                              :id   "_CATEGORY_ID_"
                                                              :type "number"}
                                                             value)
                            (= col :display)           :pie
                            (= col :made_public_by_id) (mt/user->id :crowberto)
                            (= col :embedding_params)  {:category_name "locked"}
                            (= col :public_uuid)       (str (random-uuid))
                            (= col :table_id)          (mt/id :venues)
                            (= col :source_card_id)    (:id base-card)
                            (= col :database_id)       (:id db)
                            (= col :query_type)        :native
                            (= col :type)              "model"
                            (= col :dataset_query)     (mt/mbql-query users)
                            (= col :visualization_settings) {:text "now it's a text card"}
                            (int? value)               (inc value)
                            (boolean? value)           (not value)
                            (string? value)            (str value "_changed")))]
        (doseq [col columns]
          (let [before  (select-keys card [col])
                changes {col (update-col col (get card col))}]
            ;; we'll automatically delete old revisions if we have more than [[revision/max-revisions]]
            ;; revisions for an instance, so let's clear everything to make it easier to test
            (t2/delete! :model/Revision :model "Card" :model_id (:id card))
            (t2/update! :model/Card (:id card) changes)
            (create-card-revision! (:id card) false)
            (testing (format "we should track when %s changes" col)
              (is (= 1 (t2/count :model/Revision :model "Card" :model_id (:id card)))))
            (when-not (#{;; these columns are expected to not have a description because it's always
                         ;; comes with a dataset_query changes
                         :table_id :database_id :query_type :source_card_id
                         ;; we don't need a description for made_public_by_id because whenever this field changes
                         ;; public_uuid will change and we have a description for it.
                         :made_public_by_id
                         ;; similarly, we don't need a description for `archived_directly` because whenever
                         ;; this field changes `archived` will also change and we have a description for that.
                         :archived_directly
                         ;; we don't expect a description for this column because it should never change
                         ;; once created by the migration
                         :dataset_query_metrics_v2_migration_backup} col)
              (testing (format "we should have a revision description for %s" col)
                (is (some? (u/build-sentence
                            (revision/diff-strings
                             :model/Dashboard
                             before
                             changes))))))))))))

(deftest record-revision-and-description-completeness-test-2
  ;; test tracking result_metadata for models
  (let [card-info (mt/card-with-source-metadata-for-query
                   (mt/mbql-query venues))]
    (t2.with-temp/with-temp
      [:model/Card card card-info]
      (let [before  (select-keys card [:result_metadata])
            changes (update before :result_metadata drop-last)]
        (t2/update! :model/Card (:id card) changes)
        (create-card-revision! (:id card) false)

        (testing "we should track when :result_metadata changes on model"
          (is (= 1 (t2/count :model/Revision :model "Card" :model_id (:id card)))))

        (testing "we should have a revision description for :result_metadata on model"
          (is (some? (u/build-sentence
                      (revision/diff-strings
                       :model/Dashboard
                       before
                       changes)))))))))

(deftest storing-metabase-version
  (testing "Newly created Card should know a Metabase version used to create it"
    (t2.with-temp/with-temp [:model/Card card {}]
      (is (= config/mb-version-string (:metabase_version card)))

      (with-redefs [config/mb-version-string "blablabla"]
        (t2/update! :model/Card :id (:id card) {:description "test"}))

      ;; we store version of metabase which created the card
      (is (= config/mb-version-string
             (t2/select-one-fn :metabase_version :model/Card :id (:id card)))))))

(deftest ^:parallel changed?-test
  (letfn [(changed? [before after]
            (#'card/changed? @#'card/card-compare-keys before after))]
    (testing "Ignores keyword/string"
      (is (false? (changed? {:dataset_query {:type :query}} {:dataset_query {:type "query"}}))))
    (testing "Ignores properties not in `api.card/card-compare-keys"
      (is (false? (changed? {:collection_id 1
                             :collection_position 0}
                            {:collection_id 2
                             :collection_position 1}))))
    (testing "Sees changes"
      (is (true? (changed? {:dataset_query {:type :query}}
                           {:dataset_query {:type :query
                                            :query {}}})))
      (testing "But only when they are different in the after, not just omitted"
        (is (false? (changed? {:dataset_query {} :collection_id 1}
                              {:collection_id 1})))
        (is (true? (changed? {:dataset_query {} :collection_id 1}
                             {:dataset_query nil :collection_id 1})))))))

(deftest hydrate-dashboard-count-test
  (mt/with-temp
    [:model/Card          card1 {}
     :model/Card          card2 {}
     :model/Card          card3 {}
     :model/Dashboard     dash  {}
     :model/DashboardCard _dc1  {:card_id (:id card1) :dashboard_id (:id dash)}
     :model/DashboardCard _dc2  {:card_id (:id card1) :dashboard_id (:id dash)}
     :model/DashboardCard _dc3  {:card_id (:id card2) :dashboard_id (:id dash)}]
    (is (= [2 1 0]
           (map :dashboard_count (t2/hydrate [card1 card2 card3] :dashboard_count))))))

(deftest hydrate-parameter-usage-count-test
  (mt/with-temp
    [:model/Card          card1 {}
     :model/Card          card2 {}
     :model/Card          card3 {}
     :model/ParameterCard _pc1  {:card_id (:id card1)
                                 :parameter_id              "param_1"
                                 :parameterized_object_type "card"
                                 :parameterized_object_id (:id card1)}
     :model/ParameterCard _pc2  {:card_id (:id card1)
                                 :parameter_id              "param_2"
                                 :parameterized_object_type "card"
                                 :parameterized_object_id (:id card2)}
     :model/ParameterCard _pc3  {:card_id (:id card2)
                                 :parameter_id              "param_3"
                                 :parameterized_object_type "card"
                                 :parameterized_object_id (:id card3)}]
    (is (= [2 1 0]
           (map :parameter_usage_count (t2/hydrate [card1 card2 card3] :parameter_usage_count))))))

(deftest ^:parallel average-query-time-and-last-query-started-test
  (let [now       (t/offset-date-time)
        yesterday (t/minus now (t/days 1))]
    (mt/with-temp
      [:model/Card           card {}
       :model/QueryExecution _qe1 {:card_id      (:id card)
                                   :started_at   now
                                   :cache_hit    false
                                   :running_time 50}
       :model/QueryExecution _qe2 {:card_id      (:id card)
                                   :started_at   yesterday
                                   :cache_hit    false
                                   :running_time 100}]
      (is (= 75 (-> card (t2/hydrate :average_query_time) :average_query_time int)))
      ;; the DB might save last_query_start with a different level of precision than the JVM does, on my machine
      ;; `offset-date-time` returns nanosecond precision (9 decimal places) but `last_query_start` is coming back with
      ;; microsecond precision (6 decimal places). We don't care about such a small difference, just strip it off of the
      ;; times we're comparing.
      (is (= (.withNano now 0)
             (-> (-> card (t2/hydrate :last_query_start) :last_query_start)
                 t/offset-date-time
                 (.withNano 0)))))))

(deftest save-mlv2-card-test
  (testing "App DB CRUD should work for a Card with an MLv2 query (#39024)"
    (let [metadata-provider (lib.metadata.jvm/application-database-metadata-provider (mt/id))
          venues            (lib.metadata/table metadata-provider (mt/id :venues))
          query             (lib/query metadata-provider venues)]
      (mt/with-temp [:model/Card card {:dataset_query query}]
        (testing "Save to app DB: table_id and database_id should get populated"
          (is (=? {:dataset_query {:lib/type     :mbql/query
                                   :database     (mt/id)
                                   :stages       [{:lib/type :mbql.stage/mbql, :source-table (mt/id :venues)}]
                                   :lib/metadata metadata-provider}
                   :table_id      (mt/id :venues)
                   :database_id   (mt/id)}
                  card)))
        (testing "Save to app DB: Check MLv2 query was serialized to app DB in a sane way. Metadata provider should be removed"
          (is (= {"lib/type" "mbql/query"
                  "database" (mt/id)
                  "stages"   [{"lib/type"     "mbql.stage/mbql"
                               "source-table" (mt/id :venues)}]}
                 (json/parse-string (t2/select-one-fn :dataset_query (t2/table-name :model/Card) :id (u/the-id card))))))
        (testing "fetch from app DB"
          (is (=? {:dataset_query {:lib/type     :mbql/query
                                   :database     (mt/id)
                                   :stages       [{:lib/type :mbql.stage/mbql, :source-table (mt/id :venues)}]
                                   :lib/metadata (lib.metadata.jvm/application-database-metadata-provider (mt/id))}
                   :query_type    :query
                   :table_id      (mt/id :venues)
                   :database_id   (mt/id)}
                  (t2/select-one :model/Card :id (u/the-id card)))))
        (testing "Update query: change table to ORDERS; query and table_id should reflect that"
          (let [orders (lib.metadata/table metadata-provider (mt/id :orders))]
            (is (= 1
                   (t2/update! :model/Card :id (u/the-id card)
                               {:dataset_query (lib/query metadata-provider orders)})))
            (is (=? {:dataset_query {:lib/type     :mbql/query
                                     :database     (mt/id)
                                     :stages       [{:lib/type :mbql.stage/mbql, :source-table (mt/id :orders)}]
                                     :lib/metadata (lib.metadata.jvm/application-database-metadata-provider (mt/id))}
                     :query_type    :query
                     :table_id      (mt/id :orders)
                     :database_id   (mt/id)}
                    (t2/select-one :model/Card :id (u/the-id card))))))))))

(deftest can-run-adhoc-query-test
  (let [metadata-provider (lib.metadata.jvm/application-database-metadata-provider (mt/id))
        venues            (lib.metadata/table metadata-provider (mt/id :venues))
        query             (lib/query metadata-provider venues)]
    (mt/with-current-user (mt/user->id :crowberto)
      (mt/with-temp [:model/Card card {:dataset_query query}
                     :model/Card no-query {}]
        (is (=? {:can_run_adhoc_query true}
                (t2/hydrate card :can_run_adhoc_query)))
        (is (=? {:can_run_adhoc_query false}
                (t2/hydrate no-query :can_run_adhoc_query)))))))

(deftest audit-card-permisisons-test
  (testing "Cards in audit collections are not readable or writable on OSS, even if they exist (#42645)"
    ;; Here we're testing the specific scenario where an EE instance is downgraded to OSS, but still has the audit
    ;; collections and cards installed. Since we can't load audit content on OSS, let's just redef the audit collection
    ;; to a temp collection and ensure permission checks work properly.
    (mt/with-premium-features #{}
      (mt/with-temp [:model/Collection collection {}
                     :model/Card       card       {:collection_id (:id collection)}]
        (with-redefs [audit/default-audit-collection (constantly collection)]
          (mt/with-test-user :rasta
            (is (false? (mi/can-read? card)))
            (is (false? (mi/can-write? card))))

          (mt/with-test-user :crowberto
            (is (false? (mi/can-read? card)))
            (is (false? (mi/can-write? card)))))))))

(deftest breakouts-->identifier->action-fn-test
  (are [b1 b2 expected--identifier->action] (= expected--identifier->action
                                               (#'card/breakouts-->identifier->action b1 b2))
    [[:field 10 {:temporal-unit :day}]]
    nil
    nil

    [[:expression "x" {:temporal-unit :day}]]
    nil
    nil

    [[:expression "x" {:temporal-unit :day}]]
    [[:expression "x" {:temporal-unit :month}]]
    {[:expression "x"] [:update [:expression "x" {:temporal-unit :month}]]}

    [[:expression "x" {:temporal-unit :day}]]
    [[:expression "x" {:temporal-unit :day}]]
    nil

    [[:field 10 {:temporal-unit :day}] [:expression "x" {:temporal-unit :day}]]
    [[:expression "x" {:temporal-unit :day}] [:field 10 {:temporal-unit :month}]]
    {[:field 10] [:update [:field 10 {:temporal-unit :month}]]}

    [[:field 10 {:temporal-unit :year}] [:field 10 {:temporal-unit :day-of-week}]]
    [[:field 10 {:temporal-unit :year}]]
    nil))

(deftest update-for-dashcard-fn-test
  (are [indetifier->action quasi-dashcards expected-quasi-dashcards]
       (= expected-quasi-dashcards
          (#'card/updates-for-dashcards indetifier->action quasi-dashcards))

    {[:field 10] [:update [:field 10 {:temporal-unit :month}]]}
    [{:parameter_mappings []}]
    nil

    {[:field 10] [:update [:field 10 {:temporal-unit :month}]]}
    [{:id 1 :parameter_mappings [{:target [:dimension [:field 10 nil]]}]}]
    [[1 {:parameter_mappings [{:target [:dimension [:field 10 {:temporal-unit :month}]]}]}]]

    {[:field 10] [:noop]}
    [{:id 1 :parameter_mappings [{:target [:dimension [:field 10 nil]]}]}]
    nil

    {[:field 10] [:update [:field 10 {:temporal-unit :month}]]}
    [{:id 1 :parameter_mappings [{:target [:dimension [:field 10 {:temporal-unit :year}]]}
                                 {:target [:dimension [:field 33 {:temporal-unit :month}]]}
                                 {:target [:dimension [:field 10 {:temporal-unit :day}]]}]}]
    [[1 {:parameter_mappings [{:target [:dimension [:field 10 {:temporal-unit :month}]]}
                              {:target [:dimension [:field 33 {:temporal-unit :month}]]}
                              {:target [:dimension [:field 10 {:temporal-unit :month}]]}]}]]))

(deftest update-does-not-break
  ;; There's currently a footgun in Toucan2 - if 1) the result of `before-update` doesn't have an ID, 2) part of your
  ;; `update` would change a subset of selected rows, and 3) part of your `update` would change *every* selected row
  ;; (in this case, that's the `updated_at` we automatically set), then it emits an update without a `WHERE` clause.
  ;;
  ;;This can be removed after https://github.com/camsaul/toucan2/pull/196 is merged.
  (mt/with-temp [:model/Card {card-1-id :id} {:name "Flippy"}
                 :model/Card {card-2-id :id} {:name "Dog Man"}
                 :model/Card {card-3-id :id} {:name "Petey"}]
    (testing "only the two cards specified get updated"
      (t2/update! :model/Card :id [:in [card-1-id card-2-id]]
                  {:name "Flippy"})
      (is (= "Petey" (t2/select-one-fn :name :model/Card :id card-3-id))))))