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

(ns backend-tests.rpc-file-test
  (:require
   [app.common.features :as cfeat]
   [app.common.pprint :as pp]
   [app.common.pprint :as pp]
   [app.common.thumbnails :as thc]
   [app.common.types.shape :as cts]
   [app.common.uuid :as uuid]
   [app.db :as db]
   [app.db.sql :as sql]
   [app.http :as http]
   [app.rpc :as-alias rpc]
   [app.storage :as sto]
   [app.util.time :as dt]
   [backend-tests.helpers :as th]
   [clojure.test :as t]
   [cuerdas.core :as str]))

(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)

(defn- update-file!
  [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
  (let [params {::th/type :update-file
                ::rpc/profile-id profile-id
                :id file-id
                :session-id (uuid/random)
                :revn revn
                :vern 0
                :features cfeat/supported-features
                :changes changes}
        out    (th/command! params)]
    ;; (th/print-result! out)
    (t/is (nil? (:error out)))
    (:result out)))

(t/deftest files-crud
  (let [prof    (th/create-profile* 1 {:is-active true})
        team-id (:default-team-id prof)
        proj-id (:default-project-id prof)
        file-id (uuid/next)
        page-id (uuid/next)]

    (t/testing "create file"
      (let [data {::th/type :create-file
                  ::rpc/profile-id (:id prof)
                  :project-id proj-id
                  :id file-id
                  :name "foobar"
                  :is-shared false
                  :components-v2 true}
            out (th/command! data)]

        ;; (th/print-result! out)
        (t/is (nil? (:error out)))

        (let [result (:result out)]
          (t/is (= (:name data) (:name result)))
          (t/is (= proj-id (:project-id result))))))

    (t/testing "rename file"
      (let [data {::th/type :rename-file
                  :id file-id
                  :name "new name"
                  ::rpc/profile-id (:id prof)}
            out  (th/command! data)]

        ;; (th/print-result! out)
        (let [result (:result out)]
          (t/is (= (:id data) (:id result)))
          (t/is (= (:name data) (:name result))))))

    (t/testing "query files"
      (let [data {::th/type :get-project-files
                  ::rpc/profile-id (:id prof)
                  :project-id proj-id}
            out  (th/command! data)]

        ;; (th/print-result! out)
        (t/is (nil? (:error out)))

        (let [result (:result out)]
          (t/is (= 1 (count result)))
          (t/is (= file-id (get-in result [0 :id])))
          (t/is (= "new name" (get-in result [0 :name]))))))

    (t/testing "query single file without users"
      (let [data {::th/type :get-file
                  ::rpc/profile-id (:id prof)
                  :id file-id
                  :components-v2 true}
            out  (th/command! data)]

        ;; (th/print-result! out)
        (t/is (nil? (:error out)))

        (let [result (:result out)]
          (t/is (= file-id (:id result)))
          (t/is (= "new name" (:name result)))
          (t/is (= 1 (count (get-in result [:data :pages]))))
          (t/is (nil? (:users result))))))

    (t/testing "delete file"
      (let [data {::th/type :delete-file
                  :id file-id
                  ::rpc/profile-id (:id prof)}
            out (th/command! data)]
        ;; (th/print-result! out)
        (t/is (nil? (:error out)))
        (t/is (nil? (:result out)))))

    (t/testing "query single file after delete"
      (let [data {::th/type :get-file
                  ::rpc/profile-id (:id prof)
                  :id file-id
                  :components-v2 true}
            out (th/command! data)]

        ;; (th/print-result! out)

        (let [error      (:error out)
              error-data (ex-data error)]
          (t/is (th/ex-info? error))
          (t/is (= (:type error-data) :not-found)))))

    (t/testing "query list files after delete"
      (let [data {::th/type :get-project-files
                  ::rpc/profile-id (:id prof)
                  :project-id proj-id}
            out  (th/command! data)]

        ;; (th/print-result! out)
        (t/is (nil? (:error out)))

        (let [result (:result out)]
          (t/is (= 0 (count result))))))))

(t/deftest file-gc-with-fragments
  (letfn [(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
            (let [params {::th/type :update-file
                          ::rpc/profile-id profile-id
                          :id file-id
                          :session-id (uuid/random)
                          :revn revn
                          :vern 0
                          :features cfeat/supported-features
                          :changes changes}
                  out    (th/command! params)]
              ;; (th/print-result! out)
              (t/is (nil? (:error out)))
              (:result out)))]

    (let [profile (th/create-profile* 1)
          file    (th/create-file* 1 {:profile-id (:id profile)
                                      :project-id (:default-project-id profile)
                                      :is-shared false})

          page-id  (uuid/random)
          shape-id (uuid/random)]

      ;; Preventive file-gc
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      ;; Check the number of fragments before adding the page
      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
        (t/is (= 2 (count rows))))

      ;; Add page
      (update-file!
       :file-id (:id file)
       :profile-id (:id profile)
       :revn 0
       :vern 0
       :changes
       [{:type :add-page
         :name "test"
         :id page-id}])

      ;; Check the number of fragments before adding the page
      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
        (t/is (= 3 (count rows))))

      ;; The file-gc should mark for remove unused fragments
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      ;; Check the number of fragments
      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
        (t/is (= 5 (count rows))))

      ;; The objects-gc should remove unused fragments
      (let [res (th/run-task! :objects-gc {:min-age 0})]
        (t/is (= 3 (:processed res))))

      ;; Check the number of fragments
      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
        (t/is (= 2 (count rows))))

      ;; Add shape to page that should add a new fragment
      (update-file!
       :file-id (:id file)
       :profile-id (:id profile)
       :revn 0
       :vern 0
       :changes
       [{:type :add-obj
         :page-id page-id
         :id shape-id
         :parent-id uuid/zero
         :frame-id uuid/zero
         :components-v2 true
         :obj (cts/setup-shape
               {:id shape-id
                :name "image"
                :frame-id uuid/zero
                :parent-id uuid/zero
                :type :rect})}])

      ;; Check the number of fragments
      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
        (t/is (= 3 (count rows))))

      ;; The file-gc should mark for remove unused fragments
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      ;; The objects-gc should remove unused fragments
      (let [res (th/run-task! :objects-gc {:min-age 0})]
        (t/is (= 3 (:processed res))))

      ;; Check the number of fragments;
      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)
                                                   :deleted-at nil})]
        (t/is (= 2 (count rows))))

      ;; Lets proceed to delete all changes
      (th/db-delete! :file-change {:file-id (:id file)})
      (th/db-update! :file
                     {:has-media-trimmed false}
                     {:id (:id file)})

      ;; The file-gc should remove fragments related to changes
      ;; snapshots previously deleted.
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      ;; Check the number of fragments;
      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
        ;; (pp/pprint rows)
        (t/is (= 4 (count rows)))
        (t/is (= 2 (count (remove (comp some? :deleted-at) rows)))))

      (let [res (th/run-task! :objects-gc {:min-age 0})]
        (t/is (= 2 (:processed res))))

      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
        (t/is (= 2 (count rows)))))))

(t/deftest file-gc-task-with-thumbnails
  (letfn [(add-file-media-object [& {:keys [profile-id file-id]}]
            (let [mfile  {:filename "sample.jpg"
                          :path (th/tempfile "backend_tests/test_files/sample.jpg")
                          :mtype "image/jpeg"
                          :size 312043}
                  params {::th/type :upload-file-media-object
                          ::rpc/profile-id profile-id
                          :file-id file-id
                          :is-local true
                          :name "testfile"
                          :content mfile}
                  out    (th/command! params)]

              ;; (th/print-result! out)
              (t/is (nil? (:error out)))
              (:result out)))

          (update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
            (let [params {::th/type :update-file
                          ::rpc/profile-id profile-id
                          :id file-id
                          :session-id (uuid/random)
                          :revn revn
                          :vern 0
                          :features cfeat/supported-features
                          :changes changes}
                  out    (th/command! params)]
              ;; (th/print-result! out)
              (t/is (nil? (:error out)))
              (:result out)))]

    (let [storage (:app.storage/storage th/*system*)

          profile (th/create-profile* 1)
          file    (th/create-file* 1 {:profile-id (:id profile)
                                      :project-id (:default-project-id profile)
                                      :is-shared false})

          fmo1    (add-file-media-object :profile-id (:id profile) :file-id (:id file))
          fmo2    (add-file-media-object :profile-id (:id profile) :file-id (:id file))
          shid    (uuid/random)

          page-id (first (get-in file [:data :pages]))]


      ;; Update file inserting a new image object
      (update-file!
       :file-id (:id file)
       :profile-id (:id profile)
       :revn 0
       :vern 0
       :changes
       [{:type :add-obj
         :page-id page-id
         :id shid
         :parent-id uuid/zero
         :frame-id uuid/zero
         :components-v2 true
         :obj (cts/setup-shape
               {:id shid
                :name "image"
                :frame-id uuid/zero
                :parent-id uuid/zero
                :type :image
                :metadata {:id (:id fmo1) :width 100 :height 100 :mtype "image/jpeg"}})}])

      ;; Check that reference storage objects on filemediaobjects
      ;; are the same because of deduplication feature.
      (t/is (= (:media-id fmo1) (:media-id fmo2)))
      (t/is (= (:thumbnail-id fmo1) (:thumbnail-id fmo2)))

      ;; If we launch gc-touched-task, we should have 2 items to
      ;; freeze because of the deduplication (we have uploaded 2 times
      ;; the same files).

      (let [res (th/run-task! :storage-gc-touched {:min-age 0})]
        (t/is (= 2 (:freeze res)))
        (t/is (= 0 (:delete res))))

      ;; run the file-gc task immediately without forced min-age
      (t/is (false? (th/run-task! :file-gc {:file-id (:id file)})))

      ;; run the task again
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      ;; retrieve file and check trimmed attribute
      (let [row (th/db-get :file {:id (:id file)})]
        (t/is (true? (:has-media-trimmed row))))

      ;; check file media objects
      (let [rows (th/db-query :file-media-object {:file-id (:id file)})]
        (t/is (= 2 (count rows)))
        (t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))

      (let [res (th/run-task! :objects-gc {:min-age 0})]
        (t/is (= 3 (:processed res))))

      ;; check file media objects
      (let [rows (th/db-query :file-media-object {:file-id (:id file)})]
        (t/is (= 1 (count rows)))
        (t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))

      ;; The underlying storage objects are still available.
      (t/is (some? (sto/get-object storage (:media-id fmo2))))
      (t/is (some? (sto/get-object storage (:thumbnail-id fmo2))))
      (t/is (some? (sto/get-object storage (:media-id fmo1))))
      (t/is (some? (sto/get-object storage (:thumbnail-id fmo1))))

      ;; proceed to remove usage of the file
      (update-file!
       :file-id (:id file)
       :profile-id (:id profile)
       :revn 0
       :vern 0
       :changes [{:type :del-obj
                  :page-id (first (get-in file [:data :pages]))
                  :id shid}])

      ;; Now, we have deleted the usage of pointers to the
      ;; file-media-objects, if we paste file-gc, they should be marked
      ;; as deleted.
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      (let [res (th/run-task! :objects-gc {:min-age 0})]
        (t/is (= 3 (:processed res))))

      ;; Now that file-gc have deleted the file-media-object usage,
      ;; lets execute the touched-gc task, we should see that two of
      ;; them are marked to be deleted.
      (let [res (th/run-task! :storage-gc-touched {:min-age 0})]
        (t/is (= 0 (:freeze res)))
        (t/is (= 2 (:delete res))))

      ;; Finally, check that some of the objects that are marked as
      ;; deleted we are unable to retrieve them using standard storage
      ;; public api.
      (t/is (nil? (sto/get-object storage (:media-id fmo2))))
      (t/is (nil? (sto/get-object storage (:thumbnail-id fmo2))))
      (t/is (nil? (sto/get-object storage (:media-id fmo1))))
      (t/is (nil? (sto/get-object storage (:thumbnail-id fmo1)))))))

(t/deftest file-gc-image-fills-and-strokes
  (letfn [(add-file-media-object [& {:keys [profile-id file-id]}]
            (let [mfile  {:filename "sample.jpg"
                          :path (th/tempfile "backend_tests/test_files/sample.jpg")
                          :mtype "image/jpeg"
                          :size 312043}
                  params {::th/type :upload-file-media-object
                          ::rpc/profile-id profile-id
                          :file-id file-id
                          :is-local true
                          :name "testfile"
                          :content mfile}
                  out    (th/command! params)]

              ;; (th/print-result! out)
              (t/is (nil? (:error out)))
              (:result out)))

          (update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
            (let [params {::th/type :update-file
                          ::rpc/profile-id profile-id
                          :id file-id
                          :session-id (uuid/random)
                          :revn revn
                          :vern 0
                          :components-v2 true
                          :changes changes}
                  out    (th/command! params)]
              ;; (th/print-result! out)
              (t/is (nil? (:error out)))
              (:result out)))]

    (let [storage (:app.storage/storage th/*system*)

          profile (th/create-profile* 1)
          file    (th/create-file* 1 {:profile-id (:id profile)
                                      :project-id (:default-project-id profile)
                                      :is-shared false})

          fmo1    (add-file-media-object :profile-id (:id profile) :file-id (:id file))
          fmo2    (add-file-media-object :profile-id (:id profile) :file-id (:id file))
          fmo3    (add-file-media-object :profile-id (:id profile) :file-id (:id file))
          fmo4    (add-file-media-object :profile-id (:id profile) :file-id (:id file))
          fmo5    (add-file-media-object :profile-id (:id profile) :file-id (:id file))
          s-shid  (uuid/random)
          t-shid  (uuid/random)

          page-id (first (get-in file [:data :pages]))]


      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)
                                                   :deleted-at nil})]
        (t/is (= (count rows) 1)))

      ;; Update file inserting a new image object
      (update-file!
       :file-id (:id file)
       :profile-id (:id profile)
       :revn 0
       :vern 0
       :changes
       [{:type :add-obj
         :page-id page-id
         :id s-shid
         :parent-id uuid/zero
         :frame-id uuid/zero
         :components-v2 true
         :obj (cts/setup-shape
               {:id s-shid
                :name "image"
                :frame-id uuid/zero
                :parent-id uuid/zero
                :type :image
                :metadata {:id (:id fmo1) :width 100 :height 100 :mtype "image/jpeg"}
                :fills [{:opacity 1 :fill-image {:id (:id fmo2) :width 100 :height 100 :mtype "image/jpeg"}}]
                :strokes [{:opacity 1 :stroke-image {:id (:id fmo3) :width 100 :height 100 :mtype "image/jpeg"}}]})}
        {:type :add-obj
         :page-id page-id
         :id t-shid
         :parent-id uuid/zero
         :frame-id uuid/zero
         :components-v2 true
         :obj (cts/setup-shape
               {:id t-shid
                :name "text"
                :frame-id uuid/zero
                :parent-id uuid/zero
                :type :text
                :content {:type "root"
                          :children [{:type "paragraph-set"
                                      :children [{:type "paragraph"
                                                  :children [{:fills [{:fill-opacity 1
                                                                       :fill-image {:id (:id fmo4)
                                                                                    :width 417
                                                                                    :height 354
                                                                                    :mtype "image/png"
                                                                                    :name "text fill image"}}]
                                                              :text "hi"}
                                                             {:fills [{:fill-opacity 1
                                                                       :fill-color "#000000"}]
                                                              :text "bye"}]}]}]}
                :strokes [{:opacity 1 :stroke-image {:id (:id fmo5) :width 100 :height 100 :mtype "image/jpeg"}}]})}])

      ;; run the file-gc task immediately without forced min-age
      (t/is (false? (th/run-task! :file-gc {:file-id (:id file)})))

      ;; run the task again
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      (let [res (th/run-task! :objects-gc {:min-age 0})]
        (t/is (= 2 (:processed res))))

      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)
                                                   :deleted-at nil})]
        (t/is (= (count rows) 1)))

      ;; retrieve file and check trimmed attribute
      (let [row (th/db-get :file {:id (:id file)})]
        (t/is (true? (:has-media-trimmed row))))

      ;; check file media objects
      (let [rows (th/db-exec! ["select * from file_media_object where file_id = ?" (:id file)])]
        (t/is (= 5 (count rows))))

      ;; The underlying storage objects are still available.
      (t/is (some? (sto/get-object storage (:media-id fmo5))))
      (t/is (some? (sto/get-object storage (:media-id fmo4))))
      (t/is (some? (sto/get-object storage (:media-id fmo3))))
      (t/is (some? (sto/get-object storage (:media-id fmo2))))
      (t/is (some? (sto/get-object storage (:media-id fmo1))))

      ;; proceed to remove usage of the file
      (update-file!
       :file-id (:id file)
       :profile-id (:id profile)
       :revn 0
       :vern 0
       :changes [{:type :del-obj
                  :page-id (first (get-in file [:data :pages]))
                  :id s-shid}
                 {:type :del-obj
                  :page-id (first (get-in file [:data :pages]))
                  :id t-shid}])

      ;; Now, we have deleted the usage of pointers to the
      ;; file-media-objects, if we paste file-gc, they should be marked
      ;; as deleted.
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      (let [res (th/run-task! :objects-gc {:min-age 0})]
        (t/is (= 7 (:processed res))))

      (let [rows (th/db-query :file-data-fragment {:file-id (:id file)
                                                   :deleted-at nil})]
        (t/is (= (count rows) 1)))

      ;; Now that file-gc have deleted the file-media-object usage,
      ;; lets execute the touched-gc task, we should see that two of
      ;; them are marked to be deleted.
      (let [res (th/run-task! :storage-gc-touched {:min-age 0})]
        (t/is (= 0 (:freeze res)))
        (t/is (= 2 (:delete res))))

      ;; Finally, check that some of the objects that are marked as
      ;; deleted we are unable to retrieve them using standard storage
      ;; public api.
      (t/is (nil? (sto/get-object storage (:media-id fmo5))))
      (t/is (nil? (sto/get-object storage (:media-id fmo4))))
      (t/is (nil? (sto/get-object storage (:media-id fmo3))))
      (t/is (nil? (sto/get-object storage (:media-id fmo2))))
      (t/is (nil? (sto/get-object storage (:media-id fmo1)))))))

(t/deftest file-gc-task-with-object-thumbnails
  (letfn [(insert-file-object-thumbnail! [& {:keys [profile-id file-id page-id frame-id]}]
            (let [object-id (thc/fmt-object-id file-id page-id frame-id "frame")
                  mfile     {:filename "sample.jpg"
                             :path (th/tempfile "backend_tests/test_files/sample.jpg")
                             :mtype "image/jpeg"
                             :size 312043}

                  params    {::th/type :create-file-object-thumbnail
                             ::rpc/profile-id profile-id
                             :file-id file-id
                             :object-id object-id
                             :tag "frame"
                             :media mfile}
                  out       (th/command! params)]

              ;; (th/print-result! out)
              (t/is (nil? (:error out)))
              (:result out)))

          #_(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
                          (let [params {::th/type :update-file
                                        ::rpc/profile-id profile-id
                                        :id file-id
                                        :session-id (uuid/random)
                                        :revn revn
                                        :features cfeat/supported-features
                                        :changes changes}
                                out    (th/command! params)]
              ;; (th/print-result! out)
                            (t/is (nil? (:error out)))
                            (:result out)))]

    (let [storage  (:app.storage/storage th/*system*)
          profile  (th/create-profile* 1)
          file     (th/create-file* 1 {:profile-id (:id profile)
                                       :project-id (:default-project-id profile)
                                       :is-shared false})

          file-id  (get file :id)
          page-id  (first (get-in file [:data :pages]))

          frame-id-1 (uuid/random)
          frame-id-2 (uuid/random)

          fot-1      (insert-file-object-thumbnail! :profile-id (:id profile)
                                                    :file-id file-id
                                                    :page-id page-id
                                                    :frame-id frame-id-1)
          fot-2      (insert-file-object-thumbnail! :profile-id (:id profile)
                                                    :page-id page-id
                                                    :file-id file-id
                                                    :frame-id frame-id-2)]

      ;; Add a two frames
      (update-file!
       :file-id (:id file)
       :profile-id (:id profile)
       :revn 0
       :vern 0
       :changes
       [{:type :add-obj
         :page-id page-id
         :id frame-id-1
         :parent-id uuid/zero
         :frame-id uuid/zero
         :obj (cts/setup-shape
               {:id frame-id-2
                :name "Board"
                :frame-id uuid/zero
                :parent-id uuid/zero
                :type :frame})}

        {:type :add-obj
         :page-id page-id
         :id frame-id-2
         :parent-id uuid/zero
         :frame-id uuid/zero
         :obj (cts/setup-shape
               {:id frame-id-2
                :name "Board"
                :frame-id uuid/zero
                :parent-id uuid/zero
                :type :frame})}])

      ;; Check that reference storage objects are the same because of
      ;; deduplication feature.
      (t/is (= (:media-id fot-1) (:media-id fot-2)))

      ;; If we launch gc-touched-task, we should have 1 item to freeze
      ;; because of the deduplication (we have uploaded 2 times the
      ;; same files).

      (let [res (th/run-task! :storage-gc-touched {:min-age 0})]
        (t/is (= 1 (:freeze res)))
        (t/is (= 0 (:delete res))))

      ;; run the file-gc task immediately without forced min-age
      (t/is (false? (th/run-task! :file-gc {:file-id (:id file)})))

      ;; run the task again
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      ;; retrieve file and check trimmed attribute
      (let [row (th/db-get :file {:id (:id file)})]
        (t/is (true? (:has-media-trimmed row))))

      ;; check file media objects
      (let [rows (th/db-exec! ["select * from file_tagged_object_thumbnail where file_id = ?" file-id])]
        ;; (pp/pprint rows)
        (t/is (= 2 (count rows))))

      ;; check file media objects
      (let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])]
        ;; (pp/pprint rows)
        (t/is (= 1 (count rows))))

      ;; The underlying storage objects are available.
      (t/is (some? (sto/get-object storage (:media-id fot-1))))
      (t/is (some? (sto/get-object storage (:media-id fot-2))))

      ;; proceed to remove one frame
      (update-file!
       :file-id file-id
       :profile-id (:id profile)
       :revn 0
       :vern 0
       :changes [{:type :del-obj
                  :page-id page-id
                  :id frame-id-2}])

      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})]
        (t/is (= 2 (count rows)))
        (t/is (= 1 (count (remove (comp some? :deleted-at) rows))))
        (t/is (= (thc/fmt-object-id file-id page-id frame-id-1 "frame")
                 (-> rows first :object-id))))

      ;; Now that file-gc have marked for deletion the object
      ;; thumbnail lets execute the objects-gc task which remove
      ;; the rows and mark as touched the storage object rows
      (let [res (th/run-task! :objects-gc {:min-age 0})]
        (t/is (= 5 (:processed res))))

      ;; Now that objects-gc have deleted the object thumbnail lets
      ;; execute the touched-gc task
      (let [res (th/run-task! "storage-gc-touched" {:min-age 0})]
        (t/is (= 1 (:freeze res))))

      ;; check file media objects
      (let [rows (th/db-query :storage-object {:deleted-at nil})]
        ;; (pp/pprint rows)
        (t/is (= 1 (count rows))))

      ;; proceed to remove one frame
      (update-file!
       :file-id file-id
       :profile-id (:id profile)
       :revn 0
       :vern 0
       :changes [{:type :del-obj
                  :page-id page-id
                  :id frame-id-1}])

      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})]
        (t/is (= 1 (count rows)))
        (t/is (= 0 (count (remove (comp some? :deleted-at) rows)))))

      (let [res (th/run-task! :objects-gc {:min-age 0})]
        ;; (pp/pprint res)
        (t/is (= 3 (:processed res))))

      ;; We still have th storage objects in the table
      (let [rows (th/db-query :storage-object {:deleted-at nil})]
        ;; (pp/pprint rows)
        (t/is (= 1 (count rows))))

      ;; Now that file-gc have deleted the object thumbnail lets
      ;; execute the touched-gc task
      (let [res (th/run-task! :storage-gc-touched {:min-age 0})]
        (t/is (= 1 (:delete res))))

      ;; check file media objects
      (let [rows (th/db-query :storage-object {:deleted-at nil})]
        ;; (pp/pprint rows)
        (t/is (= 0 (count rows)))))))


(t/deftest permissions-checks-creating-file
  (let [profile1 (th/create-profile* 1)
        profile2 (th/create-profile* 2)

        data     {::th/type :create-file
                  ::rpc/profile-id (:id profile2)
                  :project-id (:default-project-id profile1)
                  :name "foobar"
                  :is-shared false
                  :components-v2 true}
        out      (th/command! data)
        error    (:error out)]

    ;; (th/print-result! out)
    (t/is (th/ex-info? error))
    (t/is (th/ex-of-type? error :not-found))))

(t/deftest permissions-checks-rename-file
  (let [profile1 (th/create-profile* 1)
        profile2 (th/create-profile* 2)

        file     (th/create-file* 1 {:project-id (:default-project-id profile1)
                                     :profile-id (:id profile1)})
        data     {::th/type :rename-file
                  :id (:id file)
                  ::rpc/profile-id (:id profile2)
                  :name "foobar"}
        out      (th/command! data)
        error    (:error out)]

    ;; (th/print-result! out)
    (t/is (th/ex-info? error))
    (t/is (th/ex-of-type? error :not-found))))

(t/deftest permissions-checks-delete-file
  (let [profile1 (th/create-profile* 1)
        profile2 (th/create-profile* 2)

        file     (th/create-file* 1 {:project-id (:default-project-id profile1)
                                     :profile-id (:id profile1)})
        data     {::th/type :delete-file
                  ::rpc/profile-id (:id profile2)
                  :id (:id file)}
        out      (th/command! data)
        error    (:error out)]

    ;; (th/print-result! out)
    (t/is (th/ex-info? error))
    (t/is (th/ex-of-type? error :not-found))))

(t/deftest permissions-checks-set-file-shared
  (let [profile1 (th/create-profile* 1)
        profile2 (th/create-profile* 2)
        file     (th/create-file* 1 {:project-id (:default-project-id profile1)
                                     :profile-id (:id profile1)})
        data     {::th/type :set-file-shared
                  ::rpc/profile-id (:id profile2)
                  :id (:id file)
                  :is-shared true}
        out      (th/command! data)
        error    (:error out)]

    ;; (th/print-result! out)
    (t/is (th/ex-info? error))
    (t/is (th/ex-of-type? error :not-found))))

(t/deftest permissions-checks-link-to-library-1
  (let [profile1 (th/create-profile* 1)
        profile2 (th/create-profile* 2)
        file1    (th/create-file* 1 {:project-id (:default-project-id profile1)
                                     :profile-id (:id profile1)
                                     :is-shared true})
        file2    (th/create-file* 2 {:project-id (:default-project-id profile1)
                                     :profile-id (:id profile1)})

        data     {::th/type :link-file-to-library
                  ::rpc/profile-id (:id profile2)
                  :file-id (:id file2)
                  :library-id (:id file1)}

        out      (th/command! data)
        error    (:error out)]

      ;; (th/print-result! out)
    (t/is (th/ex-info? error))
    (t/is (th/ex-of-type? error :not-found))))

(t/deftest permissions-checks-link-to-library-2
  (let [profile1 (th/create-profile* 1)
        profile2 (th/create-profile* 2)
        file1    (th/create-file* 1 {:project-id (:default-project-id profile1)
                                     :profile-id (:id profile1)
                                     :is-shared true})

        file2    (th/create-file* 2 {:project-id (:default-project-id profile2)
                                     :profile-id (:id profile2)})

        data     {::th/type :link-file-to-library
                  ::rpc/profile-id (:id profile2)
                  :file-id (:id file2)
                  :library-id (:id file1)}

        out      (th/command! data)
        error    (:error out)]

      ;; (th/print-result! out)
    (t/is (th/ex-info? error))
    (t/is (th/ex-of-type? error :not-found))))

(t/deftest deletion
  (let [profile1 (th/create-profile* 1)
        file     (th/create-file* 1 {:project-id (:default-project-id profile1)
                                     :profile-id (:id profile1)})]
    ;; file is not deleted because it does not meet all
    ;; conditions to be deleted.
    (let [result (th/run-task! :objects-gc {:min-age 0})]
      (t/is (= 0 (:processed result))))

    ;; query the list of files
    (let [data {::th/type :get-project-files
                ::rpc/profile-id (:id profile1)
                :project-id (:default-project-id profile1)}
          out  (th/command! data)]
      ;; (th/print-result! out)
      (t/is (nil? (:error out)))
      (let [result (:result out)]
        (t/is (= 1 (count result)))))

    ;; Request file to be deleted
    (let [params {::th/type :delete-file
                  :id (:id file)
                  ::rpc/profile-id (:id profile1)}
          out    (th/command! params)]
      (t/is (nil? (:error out))))

    ;; query the list of files after soft deletion
    (let [data {::th/type :get-project-files
                ::rpc/profile-id (:id profile1)
                :project-id (:default-project-id profile1)}
          out  (th/command! data)]
      ;; (th/print-result! out)
      (t/is (nil? (:error out)))
      (let [result (:result out)]
        (t/is (= 0 (count result)))))

    ;; run permanent deletion (should be noop)
    (let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})]
      (t/is (= 0 (:processed result))))

    ;; query the list of file libraries of a after hard deletion
    (let [data {::th/type :get-file-libraries
                ::rpc/profile-id (:id profile1)
                :file-id (:id file)}
          out  (th/command! data)]
      ;; (th/print-result! out)
      (t/is (nil? (:error out)))
      (let [result (:result out)]
        (t/is (= 0 (count result)))))

    ;; run permanent deletion
    (let [result (th/run-task! :objects-gc {:min-age 0})]
      (t/is (= 1 (:processed result))))

    ;; query the list of file libraries of a after hard deletion
    (let [data {::th/type :get-file-libraries
                ::rpc/profile-id (:id profile1)
                :file-id (:id file)}
          out  (th/command! data)]
      ;; (th/print-result! out)
      (let [error (:error out)
            error-data (ex-data error)]
        (t/is (th/ex-info? error))
        (t/is (= (:type error-data) :not-found))))))


(t/deftest object-thumbnails-ops
  (let [prof (th/create-profile* 1 {:is-active true})
        file (th/create-file* 1 {:profile-id (:id prof)
                                 :project-id (:default-project-id prof)
                                 :is-shared false})
        page-id   (get-in file [:data :pages 0])
        frame1-id (uuid/next)
        shape1-id (uuid/next)
        frame2-id (uuid/next)
        shape2-id (uuid/next)

        changes   [{:type :add-obj
                    :page-id page-id
                    :id frame1-id
                    :parent-id uuid/zero
                    :frame-id uuid/zero
                    :obj (cts/setup-shape
                          {:id frame1-id
                           :use-for-thumbnail? true
                           :name "test-frame1"
                           :type :frame})}
                   {:type :add-obj
                    :page-id page-id
                    :id shape1-id
                    :parent-id frame1-id
                    :frame-id frame1-id
                    :obj (cts/setup-shape
                          {:id shape1-id
                           :name "test-shape1"
                           :type :rect})}
                   {:type :add-obj
                    :page-id page-id
                    :id frame2-id
                    :parent-id uuid/zero
                    :frame-id uuid/zero
                    :obj (cts/setup-shape
                          {:id frame2-id
                           :name "test-frame2"
                           :type :frame})}
                   {:type :add-obj
                    :page-id page-id
                    :id shape2-id
                    :parent-id frame2-id
                    :frame-id frame2-id
                    :obj (cts/setup-shape
                          {:id shape2-id
                           :name "test-shape2"
                           :type :rect})}]]
    ;; Update the file
    (th/update-file* {:file-id (:id file)
                      :profile-id (:id prof)
                      :revn 0
                      :vern 0
                      :components-v2 true
                      :changes changes})

    (t/testing "RPC page query (rendering purposes)"

      ;; Query :page RPC method without passing page-id
      (let [data {::th/type :get-page
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :features cfeat/supported-features}
            {:keys [error result] :as out} (th/command! data)]

        ;; (th/print-result! out)
        (t/is (nil? error))
        (t/is (map? result))
        (t/is (contains? result :objects))
        (t/is (contains? (:objects result) frame1-id))
        (t/is (contains? (:objects result) shape1-id))
        (t/is (contains? (:objects result) frame2-id))
        (t/is (contains? (:objects result) shape2-id))
        (t/is (contains? (:objects result) uuid/zero)))

      ;; Query :page RPC method with page-id
      (let [data {::th/type :get-page
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :page-id page-id
                  :features cfeat/supported-features}
            {:keys [error result] :as out} (th/command! data)]
        ;; (th/print-result! out)
        (t/is (map? result))
        (t/is (contains? result :objects))
        (t/is (contains? (:objects result) frame1-id))
        (t/is (contains? (:objects result) shape1-id))
        (t/is (contains? (:objects result) frame2-id))
        (t/is (contains? (:objects result) shape2-id))
        (t/is (contains? (:objects result) uuid/zero)))

      ;; Query :page RPC method with page-id and object-id
      (let [data {::th/type :get-page
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :page-id page-id
                  :object-id frame1-id
                  :features cfeat/supported-features}
            {:keys [error result] :as out} (th/command! data)]
        ;; (th/print-result! out)
        (t/is (nil? error))
        (t/is (map? result))
        (t/is (contains? result :objects))
        (t/is (contains? (:objects result) frame1-id))
        (t/is (contains? (:objects result) shape1-id))
        (t/is (not (contains? (:objects result) uuid/zero)))
        (t/is (not (contains? (:objects result) frame2-id)))
        (t/is (not (contains? (:objects result) shape2-id))))

      ;; Query :page RPC method with wrong params
      (let [data {::th/type :get-page
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :object-id frame1-id
                  :features cfeat/supported-features}
            out  (th/command! data)]

        ;; (th/print-result! out)
        (t/is (not (th/success? out)))
        (let [{:keys [type code]} (-> out :error ex-data)]
          (t/is (= :validation type))
          (t/is (= :params-validation code)))))

    (t/testing "RPC :file-data-for-thumbnail"
      ;; Insert a thumbnail data for the frame-id
      (let [data {::th/type :create-file-object-thumbnail
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :object-id (thc/fmt-object-id (:id file) page-id frame1-id "frame")
                  :media {:filename "sample.jpg"
                          :size 7923
                          :path (th/tempfile "backend_tests/test_files/sample2.jpg")
                          :mtype "image/jpeg"}}
            {:keys [error result] :as out} (th/command! data)]
        (t/is (nil? error))
        (t/is (map? result)))

      ;; Check the result
      (let [data {::th/type :get-file-data-for-thumbnail
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :features cfeat/supported-features}
            {:keys [error result] :as out} (th/command! data)]
        ;; (th/print-result! out)
        (t/is (nil? error))
        (t/is (map? result))
        (t/is (contains? result :page))
        (t/is (contains? result :revn))
        (t/is (contains? result :file-id))

        (t/is (= (:id file) (:file-id result)))
        (t/is (uuid? (get-in result [:page :objects frame1-id :thumbnail-id])))
        (t/is (= [] (get-in result [:page :objects frame1-id :shapes]))))

      ;; Delete thumbnail data
      (let [data {::th/type :delete-file-object-thumbnail
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :object-id (thc/fmt-object-id (:id file) page-id frame1-id "frame")}
            {:keys [error result] :as out} (th/command! data)]
        ;; (th/print-result! out)
        (t/is (nil? error))
        (t/is (nil? result)))

      ;; Check the result
      (let [data {::th/type :get-file-data-for-thumbnail
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :features cfeat/supported-features}
            {:keys [error result] :as out} (th/command! data)]
        ;; (th/print-result! out)
        (t/is (map? result))
        (t/is (contains? result :page))
        (t/is (contains? result :revn))
        (t/is (contains? result :file-id))
        (t/is (= (:id file) (:file-id result)))
        (t/is (nil? (get-in result [:page :objects frame1-id :thumbnail])))
        (t/is (not= [] (get-in result [:page :objects frame1-id :shapes])))))

    (t/testing "TASK :file-gc"

      ;; insert object snapshot for known frame
      (let [data {::th/type :create-file-object-thumbnail
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :object-id (thc/fmt-object-id (:id file) page-id frame1-id "frame")
                  :media {:filename "sample.jpg"
                          :size 7923
                          :path (th/tempfile "backend_tests/test_files/sample2.jpg")
                          :mtype "image/jpeg"}}
            {:keys [error result] :as out} (th/command! data)]
        (t/is (nil? error))
        (t/is (map? result)))

      ;; Wait to file be ellegible for GC
      (th/sleep 300)

      ;; run the task
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      ;; check that object thumbnails are still here
      (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
        ;; (app.common.pprint/pprint rows)
        (t/is (= 1 (count rows))))

      ;; insert object snapshot for for unknown frame
      (let [data {::th/type :create-file-object-thumbnail
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :object-id (thc/fmt-object-id (:id file) page-id (uuid/next) "frame")
                  :media {:filename "sample.jpg"
                          :size 7923
                          :path (th/tempfile "backend_tests/test_files/sample2.jpg")
                          :mtype "image/jpeg"}}
            {:keys [error result] :as out} (th/command! data)]
        (t/is (nil? error))
        (t/is (map? result)))

      ;; Mark file as modified
      (th/db-exec! ["update file set has_media_trimmed=false where id=?" (:id file)])

      ;; check that we have all object thumbnails
      (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
        ;; (app.common.pprint/pprint rows)
        (t/is (= 2 (count rows))))

      ;; run the task again
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      ;; check that we have all object thumbnails
      (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
        ;; (app.common.pprint/pprint rows)
        (t/is (= 2 (count rows))))


      ;; check that the unknown frame thumbnail is deleted
      (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
        (t/is (= 2 (count rows)))
        (t/is (= 1 (count (remove :deleted-at rows)))))

      (let [res (th/run-task! :objects-gc {:min-age 0})]
        (t/is (= 4 (:processed res))))

      (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
        ;; (app.common.pprint/pprint rows)
        (t/is (= 1 (count rows)))))))

(t/deftest file-thumbnail-ops
  (let [prof (th/create-profile* 1 {:is-active true})
        file (th/create-file* 1 {:profile-id (:id prof)
                                 :project-id (:default-project-id prof)
                                 :revn 2
                                 :vern 0
                                 :is-shared false})]

    (t/testing "create a file thumbnail"
      ;; insert object snapshot for known frame
      (let [data {::th/type :create-file-thumbnail
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :revn 1
                  :media {:filename "sample.jpg"
                          :size 7923
                          :path (th/tempfile "backend_tests/test_files/sample2.jpg")
                          :mtype "image/jpeg"}}
            {:keys [error result] :as out} (th/command! data)]

        ;; (th/print-result! out)
        (t/is (nil? error))
        (t/is (map? result)))

      ;; insert another thumbnail with different revn
      (let [data {::th/type :create-file-thumbnail
                  ::rpc/profile-id (:id prof)
                  :file-id (:id file)
                  :revn 2
                  :media {:filename "sample.jpg"
                          :size 7923
                          :path (th/tempfile "backend_tests/test_files/sample2.jpg")
                          :mtype "image/jpeg"}}
            {:keys [error result] :as out} (th/command! data)]

        ;; (th/print-result! out)
        (t/is (nil? error))
        (t/is (map? result)))

      (let [rows (th/db-query :file-thumbnail {:file-id (:id file)})]
        (t/is (= 2 (count rows)))))

    (t/testing "gc task"
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      (let [rows (th/db-query :file-thumbnail {:file-id (:id file)})]
        (t/is (= 2 (count rows)))
        (t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))

      (let [res (th/run-task! :objects-gc {:min-age 0})]
        (t/is (= 2 (:processed res))))

      (let [rows (th/db-query :file-thumbnail {:file-id (:id file)})]
        (t/is (= 1 (count rows)))))))

(t/deftest file-tiered-storage
  (let [profile (th/create-profile* 1)
        file    (th/create-file* 1 {:profile-id (:id profile)
                                    :project-id (:default-project-id profile)
                                    :is-shared false})

        page-id  (uuid/random)
        shape-id (uuid/random)]

      ;; Preventive file-gc
    (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      ;; Preventive objects-gc
    (let [result (th/run-task! :objects-gc {:min-age 0})]
      (t/is (= 1 (:processed result))))

      ;; Check the number of fragments before adding the page
    (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
      (t/is (= 1 (count rows)))
      (t/is (every? #(some? (:data %)) rows)))

      ;; Mark the file ellegible again for GC
    (th/db-update! :file
                   {:has-media-trimmed false}
                   {:id (:id file)})

      ;; Run FileGC again, with tiered storage activated
    (with-redefs [app.config/flags (conj app.config/flags :tiered-file-data-storage)]
      (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

        ;; The FileGC task will schedule an inner taskq
      (th/run-pending-tasks!))

      ;; Clean objects after file-gc
    (let [result (th/run-task! :objects-gc {:min-age 0})]
      (t/is (= 1 (:processed result))))

      ;; Check the number of fragments before adding the page
    (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
      (t/is (= 1 (count rows)))
      (t/is (every? #(nil? (:data %)) rows))
      (t/is (every? #(uuid? (:data-ref-id %)) rows))
      (t/is (every? #(= "objects-storage" (:data-backend %)) rows)))

    (let [file    (th/db-get :file {:id (:id file)})
          storage (sto/resolve th/*system*)]
      (t/is (= "objects-storage" (:data-backend file)))
      (t/is (nil? (:data file)))
      (t/is (uuid? (:data-ref-id file)))

      (let [sobj (sto/get-object storage (:data-ref-id file))]
        (t/is (= "file-data" (:bucket (meta sobj))))
        (t/is (= (:id file) (:file-id (meta sobj))))))

      ;; Add shape to page that should load from cold storage again into the hot storage (db)
    (update-file!
     :file-id (:id file)
     :profile-id (:id profile)
     :revn 0
     :vern 0
     :changes
     [{:type :add-page
       :name "test"
       :id page-id}])

      ;; Check the number of fragments
    (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
      (t/is (= 2 (count rows))))

      ;; Check the number of fragments
    (let [[row1 row2 :as rows]
          (th/db-query :file-data-fragment
                       {:file-id (:id file)
                        :deleted-at nil}
                       {:order-by [:created-at]})]
        ;; (pp/pprint rows)
      (t/is (= 2 (count rows)))
      (t/is (nil? (:data row1)))
      (t/is (= "objects-storage" (:data-backend row1)))
      (t/is (bytes? (:data row2)))
      (t/is (nil? (:data-backend row2))))

      ;; The file-gc should mark for remove unused fragments
    (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))

      ;; The objects-gc should remove unused fragments
    (let [res (th/run-task! :objects-gc {:min-age 0})]
      (t/is (= 2 (:processed res))))

      ;; Check the number of fragments before adding the page
    (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
      (t/is (= 2 (count rows)))
      (t/is (every? #(bytes? (:data %)) rows))
      (t/is (every? #(nil? (:data-ref-id %)) rows))
      (t/is (every? #(nil? (:data-backend %)) rows)))))