;; 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 common-tests.files.comp-processors-test
  (:require
   [app.common.data :as d]
   [app.common.files.comp-processors :as cfcp]
   [app.common.test-helpers.components :as thc]
   [app.common.test-helpers.compositions :as tho]
   [app.common.test-helpers.files :as thf]
   [app.common.test-helpers.ids-map :as thi]
   [app.common.test-helpers.shapes :as ths]
   [app.common.types.component :as ctk]
   [app.common.types.components-list :as ctkl]
   [app.common.types.file :as ctf]
   [clojure.test :as t]))

(t/deftest test-remove-unneeded-objects-in-components

  (t/testing "nil file should return nil"
    (let [file  nil
          file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)]
      (t/is (nil? file'))))

  (t/testing "empty file should not need any action"
    (let [file  (thf/sample-file :file1)
          file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)]
      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file without components should not need any action"
    (let [file
          (-> (thf/sample-file :file1)
              (tho/add-frame-with-child :frame1 :shape1))

          file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)]

      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with non deleted components should not need any action"
    (let [file
          (-> (thf/sample-file :file1)
              (tho/add-simple-component :component1 :frame1 :shape1))

          file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)]

      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with deleted components should not need any action"
    (let [file
          (-> (thf/sample-file :file1)
              (tho/add-simple-component :component1 :frame1 :shape1)
              (tho/delete-shape :frame1))

          file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)]

      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with non deleted components with :objects nil should remove it"
    (let [file
          (-> (thf/sample-file :file1)
              (tho/add-simple-component :component1 :frame1 :shape1)
              (thc/update-component :component1 {:objects nil}))

          file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)

          diff (d/map-diff file file')

          expected-diff {:data
                         {:components
                          {(thi/id :component1)
                           {}}}}]

      (t/is (= expected-diff diff))))

  (t/testing "file with non deleted components with :objects should remove it"
    (let [file
          (-> (thf/sample-file :file1)
              (tho/add-simple-component :component1 :frame1 :shape1)
              (thc/update-component :component1 {:objects {:sample 777}}))

          file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)

          diff (d/map-diff file file')

          expected-diff {:data
                         {:components
                          {(thi/id :component1)
                           {:objects
                            [{:sample 777} nil]}}}}]

      (t/is (= expected-diff diff))))

  (t/testing "file with deleted components without :objects should add an empty one"
    (let [file
          (-> (thf/sample-file :file1)
              (tho/add-simple-component :component1 :frame1 :shape1)
              (tho/delete-shape :frame1)
              (ctf/update-file-data
               (fn [file-data]
                 (ctkl/update-component file-data (thi/id :component1) #(dissoc % :objects)))))

          file' (ctf/update-file-data file cfcp/remove-unneeded-objects-in-components)

          diff (d/map-diff file file')

          expected-diff {:data
                         {:components
                          {(thi/id :component1)
                           {:objects
                            [nil {}]}}}}]

      (t/is (= expected-diff diff)))))

(t/deftest test-normalize-component-root

  (t/testing "nil file should return nil"
    (let [file  nil
          file' (ctf/update-file-data file cfcp/normalize-component-root)]
      (t/is (nil? file'))))

  (t/testing "empty file should not need any action"
    (let [file  (thf/sample-file :file1)
          file' (ctf/update-file-data file cfcp/normalize-component-root)]
      (t/is (empty? (d/map-diff file file')))))

  (t/testing "shape with :component-root true should not be modified"
    (let [file
          (-> (thf/sample-file :file1)
              (tho/add-simple-component :component1 :main1-root :main1-child)
              (thc/instantiate-component :component1 :copy1-root))

          file' (ctf/update-file-data file cfcp/normalize-component-root)]

      (t/is (empty? (d/map-diff file file')))))

  (t/testing "shape with :component-root false should have it removed"
    (let [file
          (-> (thf/sample-file :file1)
              (tho/add-nested-component-with-copy :component1 :main1-root :main1-child
                                                  :component2 :main2-root :nested-head
                                                  :copy2-root)
              (ths/update-shape :nested-head :component-root false))

          file' (ctf/update-file-data file cfcp/normalize-component-root)

          shape' (ths/get-shape file' :nested-head)]

      (t/is (not (contains? shape' :component-root))))))

(t/deftest test-migration-0022-fixes-component-root-false-mismatch

  (t/testing "a nested copy with :component-root false and a stale :component-id is fixed by the full migration sequence"
    (let [file
          ;; Reproduce the legacy corruption: a nested copy head has
          ;; :component-root false (instead of absent) AND a stale
          ;; component-id. Migrations 0019/0020 alone don't catch it
          ;; because subcopy-head? expects nil. After normalize-component-root,
          ;; sync-component-id-with-ref-shape can repair it.
          (-> (thf/sample-file :file1)
              (tho/add-nested-component :component1 :main1-root :main1-child
                                        :component2 :main2-root :nested-head)
              (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
              (ths/update-shape :copy2-nested-head :component-root false)
              (ths/update-shape :copy2-nested-head :component-id (thi/new-id! :wrong-id)))

          ;; Run the old sequence (0019/0020 without normalization): the bad shape is skipped.
          old-sequence
          (-> file
              (ctf/update-file-data #(cfcp/fix-missing-swap-slots % {}))
              (ctf/update-file-data #(cfcp/sync-component-id-with-ref-shape % {})))

          old-shape (ths/get-shape old-sequence :copy2-nested-head)

          ;; Run the new sequence (0022): normalize first, then 0019/0020 logic.
          new-sequence
          (-> file
              (ctf/update-file-data cfcp/normalize-component-root)
              (ctf/update-file-data #(cfcp/fix-missing-swap-slots % {}))
              (ctf/update-file-data #(cfcp/sync-component-id-with-ref-shape % {})))

          new-shape (ths/get-shape new-sequence :copy2-nested-head)]

      ;; Confirm the old sequence fails to repair (this guards against a future
      ;; subcopy-head? change accidentally fixing it and masking the regression).
      (t/is (= false (:component-root old-shape)))
      (t/is (= (thi/id :wrong-id) (:component-id old-shape)))

      ;; Confirm the new sequence repairs both the explicit false and the bad ref.
      (t/is (not (contains? new-shape :component-root)))
      (t/is (= (thi/id :component1) (:component-id new-shape))))))

(t/deftest test-fix-missing-swap-slots

  (t/testing "nil file should return nil"
    (let [file  nil
          file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]
      (t/is (nil? file'))))

  (t/testing "empty file should not need any action"
    (let [file  (thf/sample-file :file1)
          file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]
      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file without components should not need any action"
    (let [file
          ;; :frame1 [:name Frame1]
          ;;     :child1 [:name Rect1]
          (-> (thf/sample-file :file1)
              (tho/add-frame-with-child :frame1 :shape1))

          file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]

      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with nested not swapped components should not need any action"
    (let [file
          ;; {:main1-root} [:name Frame1]      # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]      # [Component :component2]
          ;;     :nested-head [:name Frame1]   @--> [Component :component1] :main1-root
          ;;         <no-label> [:name Rect1]        ---> :main1-child
          ;;
          ;; :copy2-root [:name Frame2]        #--> [Component :component2] :main2-root
          ;;     <no-label> [:name Frame1]           @--> [Component :component1] :nested-head
          ;;         <no-label> [:name Rect1]        ---> <no-label>
          (-> (thf/sample-file :file1)
              (tho/add-nested-component-with-copy :component1 :main1-root :main1-child
                                                  :component2 :main2-root :nested-head
                                                  :copy2-root))

          file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]

      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with a normally swapped copy should not need any action"
    (let [file
          ;; {:main1-root} [:name Frame1]           # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]           # [Component :component2]
          ;;     :nested-head [:name Frame1]        @--> [Component :component1] :main1-root
          ;;         <no-label> [:name Rect1]             ---> :main1-child
          ;;
          ;; {:main3-root} [:name Frame3]           # [Component :component3]
          ;;     :main3-child [:name Rect3]
          ;;
          ;; :copy2-root [:name Frame2]             #--> [Component :component2] :main2-root
          ;;     :copy2-nested-head [:name Frame3]  @--> [Component :component3] :main3-root
          ;;                                             {swap-slot :nested-head}
          ;;         <no-label> [:name Rect3]        ---> :main3-child
          (-> (thf/sample-file :file1)
              (tho/add-nested-component :component1 :main1-root :main1-child
                                        :component2 :main2-root :nested-head)
              (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
              (tho/add-simple-component :component3 :main3-root :main3-child
                                        :root-params {:name "Frame3"}
                                        :child-params {:name "Rect3"})
              (tho/swap-component-in-first-child :copy2-root :component3))

          file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]

      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with a swapped nested copy in a main should not need any action"
    (let [file
          ;; {:main1-root} [:name Frame1]           # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main3-root} [:name Frame3]           # [Component :component3]
          ;;     :main3-child [:name Rect3]
          ;;
          ;; {:main2-root} [:name Frame2]           # [Component :component2]
          ;;     :nested-head [:name Frame3]        @--> [Component :component3] :main3-root
          ;;                                             {swap-slot :nested-head}
          ;;         <no-label> [:name Rect3]             ---> :main3-child
          ;;
          ;; :copy2-root [:name Frame2]             #--> [Component :component2] :main2-root
          ;;     :copy2-nested-head [:name Frame3]  @--> [Component :component3] :nested-head
          ;;         <no-label> [:name Rect3]        ---> <no-label>
          (-> (thf/sample-file :file1)
              (tho/add-nested-component :component1 :main1-root :main1-child
                                        :component2 :main2-root :nested-head)
              (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
              (tho/add-simple-component :component3 :main3-root :main3-child
                                        :root-params {:name "Frame3"}
                                        :child-params {:name "Rect3"})
              (tho/swap-component-in-shape :nested-head :component3
                                           :propagate-fn #(tho/propagate-component-changes % :component2)))

          file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]

      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with a swapped copy with broken slot should have it repaired"
    (let [file
          ;; {:main1-root} [:name Frame1]           # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]           # [Component :component2]
          ;;     :nested-head [:name Frame1]        @--> [Component :component1] :main1-root
          ;;         <no-label> [:name Rect1]             ---> :main1-child
          ;;
          ;; {:main3-root} [:name Frame3]           # [Component :component3]
          ;;     :main3-child [:name Rect3]
          ;;
          ;; :copy2-root [:name Frame2]             #--> [Component :component2] :main2-root
          ;;     :copy2-nested-head [:name Frame3]  @--> [Component :component3] :main3-root
          ;;                                             NO SWAP SLOT
          ;;         <no-label> [:name Rect3]        ---> :main3-child
          (-> (thf/sample-file :file1)
              (tho/add-nested-component :component1 :main1-root :main1-child
                                        :component2 :main2-root :nested-head)
              (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
              (tho/add-simple-component :component3 :main3-root :main3-child
                                        :root-params {:name "Frame3"}
                                        :child-params {:name "Rect3"})
              (tho/swap-component-in-first-child :copy2-root :component3)
              (ths/update-shape :copy2-nested-head :touched nil))

          file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))

          diff (d/map-diff file file')

          expected-diff {:data
                         {:pages-index
                          {(thf/current-page-id file)
                           {:objects
                            {(thi/id :copy2-nested-head)
                             {:touched
                              [nil
                               #{(ctk/build-swap-slot-group (str (thi/id :nested-head)))}]}}}}}}]

      (t/is (= expected-diff diff))))

  (t/testing "file with a swapped copy inside a main with broken slot has no effect since it cannot be distinguished"
    (let [file
          ;; {:main1-root} [:name Frame1]           # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main3-root} [:name Frame3]           # [Component :component3]
          ;;     :main3-child [:name Rect3]
          ;;
          ;; {:main2-root} [:name Frame2]           # [Component :component2]
          ;;     :nested-head [:name Frame3]        @--> [Component :component3] :main3-root
          ;;                                             NO SWAP SLOT
          ;;         <no-label> [:name Rect3]             ---> :main3-child
          ;;
          ;; :copy2-root [:name Frame2]             #--> [Component :component2] :main2-root
          ;;     :copy2-nested-head [:name Frame3]  @--> [Component :component3] :nested-head
          ;;         <no-label> [:name Rect3]        ---> <no-label>
          (-> (thf/sample-file :file1)
              (tho/add-nested-component :component1 :main1-root :main1-child
                                        :component2 :main2-root :nested-head)
              (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
              (tho/add-simple-component :component3 :main3-root :main3-child
                                        :root-params {:name "Frame3"}
                                        :child-params {:name "Rect3"})
              (tho/swap-component-in-shape :nested-head :component3
                                           :propagate-fn #(tho/propagate-component-changes % :component2))
              (ths/update-shape :nested-head :touched nil))

          file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))]

      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with a two levels nested copy in a main swapped with broken slot should have it repaired"
    (let [file
          ;; {:main1-root} [:name Frame1]            # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]            # [Component :component2]
          ;;     :nested-head1 [:name Frame1]        @--> [Component :component1] :main1-root
          ;;         <no-label> [:name Rect1]        ---> :main1-child
          ;;
          ;; {:main4-root} [:name Frame4]            # [Component :component4]
          ;;     :main4-child [:name Rect4]
          ;;
          ;; {:main3-root} [:name Frame3]            # [Component :component3]
          ;;     :nested-head2 [:name Frame2]        @--> [Component :component2] :main2-root
          ;;         :nested-subhead2 [:name Frame4] @--> [Component :component4] :main4-root
          ;;                                             NO SWAP SLOT
          ;;             <no-label> [:name Rect4]    ---> :main4-child
          ;;
          ;; :copy2-root [:name Frame3]              #--> [Component :component3] :main3-root
          ;;     <no-label> [:name Frame2]           @--> [Component :component2] :nested-head2
          ;;         <no-label> [:name Frame4]       @--> [Component :component4] :nested-subhead2
          ;;             <no-label> [:name Rect4]    ---> <no-label>
          (-> (thf/sample-file :file1)
              (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child
                                                             :component2 :main2-root :nested-head1
                                                             :component3 :main3-root :nested-head2 :nested-subhead2
                                                             :copy2-root)
              (tho/add-simple-component :component4 :main4-root :main4-child
                                        :root-params {:name "Frame4"}
                                        :child-params {:name "Rect4"})
              (tho/swap-component-in-shape :nested-subhead2 :component4
                                           :propagate-fn #(tho/propagate-component-changes % :component3))
              (ths/update-shape :nested-subhead2 :touched nil))

          file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % {}))

          diff (d/map-diff file file')

          expected-diff {:data
                         {:pages-index
                          {(thf/current-page-id file)
                           {:objects
                            {(thi/id :nested-subhead2)
                             {:touched
                              [nil
                               #{(ctk/build-swap-slot-group (str (thi/id :nested-head1)))}]}}}}}}]

      (t/is (= expected-diff diff))))

  (t/testing "when components are in external libraries, the fix still works well"
    (let [library1
          ;; {:main1-root} [:name Frame1]           # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]           # [Component :component2]
          ;;     :nested2-head [:name Frame1]       @--> [Component :component1] :main1-root
          ;;         :nested2-child [:name Rect1]   ---> :main1-child
          (-> (thf/sample-file :library1)
              (tho/add-nested-component :component1 :main1-root :main1-child
                                        :component2 :main2-root :nested2-head
                                        :nested-head-params {:children-labels [:nested2-child]}))
          library2
          ;; {:main3-root} [:name Frame3]           # [Component :component3]
          ;;     :main3-child [:name Rect3]
          ;;
          ;; {:main4-root} [:name Frame4]           # [Component :component4]
          ;;     :nested4-head [:name Frame3]       @--> [Component :component1] :main3-root
          ;;         :nested4-child [:name Rect3]   ---> :main3-child
          (-> (thf/sample-file :library2)
              (tho/add-nested-component :component3 :main3-root :main3-child
                                        :component4 :main4-root :nested4-head
                                        :root1-params {:name "Frame3"}
                                        :main1-child-params {:name "Rect3"}
                                        :main2-root-params {:name "Frame4"}
                                        :nested-head-params {:children-labels [:nested4-child]}))

          file
          ;; :copy2 [:name Frame2]                  #--> [Component :component2] :main2-root
          ;;     :copy2-nested-head [:name Frame4]  @--> [Component :component4] :main4-root
          ;;                                             NO SWAP SLOT
          ;;         <no-label> [:name Frame3]      @--> :nested4-head
          ;;             <no-label> [:name Rect3]   ---> :nested4-child
          (-> (thf/sample-file :file1)
              (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head]
                                         :library library1)
              (tho/swap-component-in-first-child :copy2 :component4 :library library2)
              (ths/update-shape :copy2-nested-head :touched nil))

          libraries {(:id library1) library1
                     (:id library2) library2}

          file' (ctf/update-file-data file #(cfcp/fix-missing-swap-slots % libraries))

          diff (d/map-diff file file')

          expected-diff {:data
                         {:pages-index
                          {(thf/current-page-id file)
                           {:objects
                            {(thi/id :copy2-nested-head)
                             {:touched
                              [nil
                               #{(ctk/build-swap-slot-group (str (thi/id :nested2-head)))}]}}}}}}]

      (t/is (= expected-diff diff)))))

(t/deftest test-sync-component-id-with-ref-shape

  (t/testing "nil file should return nil"
    (let [file  nil
          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))]
      (t/is (nil? file'))))

  (t/testing "empty file should not need any action"
    (let [file  (thf/sample-file :file1)
          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))]
      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file without components should not need any action"
    (let [file
          ;; :frame1 [:name Frame1]
          ;;     :child1 [:name Rect1]
          (-> (thf/sample-file :file1)
              (tho/add-frame-with-child :frame1 :shape1))

          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))]

      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with valid normal components should not need any action"
    (let [file
          ;; {:main1-root} [:name Frame1]            # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]            # [Component :component2]
          ;;     :nested-head1 [:name Frame1]        @--> [Component :component1] :main1-root
          ;;         <no-label> [:name Rect1]        ---> :main1-child
          ;;
          ;; {:main3-root} [:name Frame3]            # [Component :component3]
          ;;     :nested-head2 [:name Frame2]        @--> [Component :component2] :main2-root
          ;;         :nested-subhead2 [:name Frame1] @--> [Component :component1] :nested-head1
          ;;             <no-label> [:name Rect1]    ---> <no-label>
          ;;
          ;; :copy2-root [:name Frame3]              #--> [Component :component3] :main3-root
          ;;     <no-label> [:name Frame2]           @--> [Component :component2] :nested-head2
          ;;         <no-label> [:name Frame1]       @--> [Component :component1] :nested-subhead2
          ;;             <no-label> [:name Rect1]    ---> <no-label>
          (-> (thf/sample-file :file1)
              (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child
                                                             :component2 :main2-root :nested-head1
                                                             :component3 :main3-root :nested-head2 :nested-subhead2
                                                             :copy2-root))

          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))]

      #_(thf/dump-file file') ;; Uncomment to debug
      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with valid swapped components should not need any action"
    (let [file
          ;; {:main1-root} [:name Frame1]      # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]      # [Component :component2]
          ;;     :nested-head [:name Frame1]   @--> [Component :component1] :main1-root
          ;;         <no-label> [:name Rect1]        ---> :main1-child
          ;;
          ;; {:main3-root} [:name Frame3]      # [Component :component3]
          ;;     :main3-child [:name Rect3]
          ;;
          ;; :copy2-root [:name Frame2]        #--> [Component :component2] :main2-root
          ;;     <no-label> [:name Frame1]           @--> [Component :component1] :nested-head
          ;;         <no-label> [:name Rect1]        ---> <no-label>
          ;;
          ;; :copy3-root [:name Frame2]             #--> [Component :component2] :main2-root
          ;;     :copy3-nested-head [:name Frame3]  @--> [Component :component3] :main3-root
          ;;                                             {swap-slot :nested-head}
          ;;         <no-label> [:name Rect3]        ---> :main3-child
          (-> (thf/sample-file :file1)
              (tho/add-nested-component-with-copy :component1 :main1-root :main1-child
                                                  :component2 :main2-root :nested-head
                                                  :copy2-root)
              (tho/add-simple-component :component3 :main3-root :main3-child
                                        :root-params {:name "Frame3"}
                                        :child-params {:name "Rect3"})
              (thc/instantiate-component :component2 :copy3-root :children-labels [:copy3-nested-head])
              (tho/swap-component-in-first-child :copy3-root :component3))

          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))]

      #_(thf/dump-file file') ;; Uncomment to debug
      (t/is (empty? (d/map-diff file file')))))

  (t/testing "file with a non swapped copy with broken component id/file should have it repaired"
    (let [file
          ;; {:main1-root} [:name Frame1]      # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]      # [Component :component2]
          ;;     :nested-head [:name Frame1]   @--> [Component :component1] :main1-root
          ;;         <no-label> [:name Rect1]        ---> :main1-child
          ;;
          ;; :copy2-root [:name Frame2]        #--> [Component :component2] :main2-root
          ;;    :copy2-nested-head [:name Frame1]    @--> [Component <bad>] :nested-head  ## <- BAD component-id
          ;;         <no-label> [:name Rect1]        ---> <no-label>
          ;;
          ;; :copy3-root [:name Frame2]        #--> [Component :component2] :main2-root
          ;;    :copy3-nested-head [:name Frame1]    @--> [Component <bad>] :nested-head  ## <- BAD component-file
          ;;         <no-label> [:name Rect1]        ---> <no-label>
          (-> (thf/sample-file :file1)
              (tho/add-nested-component :component1 :main1-root :main1-child
                                        :component2 :main2-root :nested-head)
              (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
              (thc/instantiate-component :component2 :copy3-root :children-labels [:copy3-nested-head])
              (ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id))
              (ths/update-shape :copy3-nested-head :component-file (thi/new-id! :some-other-file)))

          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))

          diff (d/map-diff file file')

          expected-diff {:data
                         {:pages-index
                          {(thf/current-page-id file)
                           {:objects
                            {(thi/id :copy2-nested-head)
                             {:component-id
                              [(thi/id :some-other-id) (thi/id :component1)]}
                             (thi/id :copy3-nested-head)
                             {:component-file
                              [(thi/id :some-other-file) (thi/id :file1)]}}}}}}]

      #_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug
      (t/is (= expected-diff diff))))

  (t/testing "file with a copy of a swapped main with broken component id/file should have it repaired"
    (let [file
          ;; {:main1-root} [:name Frame1]           # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main3-root} [:name Frame3]           # [Component :component3]
          ;;     :main3-child [:name Rect3]
          ;;
          ;; {:main2-root} [:name Frame2]           # [Component :component2]
          ;;     :nested-head [:name Frame3]        @--> [Component :component3] :main3-root
          ;;                                             {swap-slot :nested-head}
          ;;         <no-label> [:name Rect3]       ---> :main3-child
          ;;
          ;; :copy2-root [:name Frame2]             #--> [Component :component2] :main2-root
          ;;     :copy2-nested-head [:name Frame3]  @--> [Component: <bad>] :nested-head   ## <- BAD component-id/file
          ;;         <no-label> [:name Rect3]       ---> <no-label>
          (-> (thf/sample-file :file1)
              (tho/add-nested-component :component1 :main1-root :main1-child
                                        :component2 :main2-root :nested-head)
              (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
              (tho/add-simple-component :component3 :main3-root :main3-child
                                        :root-params {:name "Frame3"}
                                        :child-params {:name "Rect3"})
              (tho/swap-component-in-shape :nested-head :component3
                                           :propagate-fn #(tho/propagate-component-changes % :component2))
              (ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id))
              (ths/update-shape :copy2-nested-head :component-file (thi/new-id! :some-other-file)))

          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))

          diff (d/map-diff file file')

          expected-diff {:data
                         {:pages-index
                          {(thf/current-page-id file)
                           {:objects
                            {(thi/id :copy2-nested-head)
                             {:component-id
                              [(thi/id :some-other-id) (thi/id :component3)]
                              :component-file
                              [(thi/id :some-other-file) (thi/id :file1)]}}}}}}]

      #_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug
      (t/is (= expected-diff diff))))

  (t/testing "file with multiple copies of same component should sync all"
    (let [file
          (-> (thf/sample-file :file1)
              (tho/add-simple-component :component1 :frame1 :shape1)
              (thc/instantiate-component :component1 :copy1-root :children-labels [:copy1-child])
              (thc/instantiate-component :component1 :copy2-root :children-labels [:copy2-child])
              (ths/update-shape :copy1-child :component-id (thi/new-id! :wrong-id1))
              (ths/update-shape :copy2-child :component-id (thi/new-id! :wrong-id2)))

          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))

          diff (d/map-diff file file')]

      ;; Both copies should be corrected
      (t/is (contains? diff :data))
      (t/is (contains? (get-in diff [:data :pages-index]) (thf/current-page-id file)))))

  (t/testing "file with a copy root with broken component id/file cannot be repaired. But it's propagated to copies."
    (let [file
          ;; {:main1-root} [:name Frame1]      # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]      # [Component :component2]
          ;;     :nested-head [:name Frame1]   @--> [Component <bad>] :main1-root  ## <- BAD component-id/file
          ;;         <no-label> [:name Rect1]        ---> :main1-child
          ;;
          ;; :copy2-root [:name Frame2]        #--> [Component :component2] :main2-root
          ;;    :copy2-nested-head [:name Frame1]    @--> [Component :component1] :nested-head
          ;;         <no-label> [:name Rect1]        ---> <no-label>
          (-> (thf/sample-file :file1)
              (tho/add-nested-component :component1 :main1-root :main1-child
                                        :component2 :main2-root :nested-head)
              (thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
              (ths/update-shape :nested-head :component-id (thi/new-id! :some-other-id))
              (ths/update-shape :nested-head :component-file (thi/new-id! :some-other-file)))

          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))

          diff (d/map-diff file file')

          expected-diff {:data
                         {:pages-index
                          {(thf/current-page-id file)
                           {:objects
                            {(thi/id :copy2-nested-head)
                             {:component-id
                              [(thi/id :component1) (thi/id :some-other-id)]
                              :component-file
                              [(thi/id :file1) (thi/id :some-other-file)]}}}}}}]

      (t/is (= expected-diff diff))))

  (t/testing "file with a 2nd nested copy inside a main with broken component/id should have it repaired, and propagated to copies"
    (let [file
          ;; {:main1-root} [:name Frame1]            # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]            # [Component :component2]
          ;;     :nested-head1 [:name Frame1]        @--> [Component :component1] :main1-root
          ;;         <no-label> [:name Rect1]        ---> :main1-child
          ;;
          ;; {:main3-root} [:name Frame3]            # [Component :component3]
          ;;     :nested-head2 [:name Frame2]        @--> [Component :component2] :main2-root
          ;;         :nested-subhead2 [:name Frame1] @--> [Component <bad>] :nested-head1  ## <- BAD component-id/file
          ;;             <no-label> [:name Rect1]    ---> <no-label>
          ;;
          ;; :copy2-root [:name Frame3]              #--> [Component :component3] :main3-root
          ;;     <no-label> [:name Frame2]           @--> [Component :component2] :nested-head2
          ;;         <no-label> [:name Frame1]       @--> [Component :component1] :nested-subhead2
          ;;             <no-label> [:name Rect1]    ---> <no-label>
          (-> (thf/sample-file :file1)
              (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child
                                                             :component2 :main2-root :nested-head1
                                                             :component3 :main3-root :nested-head2 :nested-subhead2
                                                             :copy2-root)
              (ths/update-shape :nested-subhead2 :component-id (thi/new-id! :some-other-id))
              (ths/update-shape :nested-subhead2 :component-file (thi/new-id! :some-other-file)))

          copy2-root (ths/get-shape file :copy2-root)
          copy2-root-child1 (ths/get-shape-by-id file (first (:shapes copy2-root)))
          copy2-root-child2 (ths/get-shape-by-id file (first (:shapes copy2-root-child1)))
          file (-> file
                   (ths/update-shape-by-id (:id copy2-root-child2) :component-id (thi/id :some-other-id))
                   (ths/update-shape-by-id (:id copy2-root-child2) :component-file (thi/id :some-other-file)))

          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))

          diff (d/map-diff file file')

          expected-diff {:data
                         {:pages-index
                          {(thf/current-page-id file)
                           {:objects
                            {(thi/id :nested-subhead2)
                             {:component-id
                              [(thi/id :some-other-id) (thi/id :component1)]
                              :component-file
                              [(thi/id :some-other-file) (thi/id :file1)]}
                             (:id copy2-root-child2)
                             {:component-id
                              [(thi/id :some-other-id) (thi/id :component1)]
                              :component-file
                              [(thi/id :some-other-file) (thi/id :file1)]}}}}}}]

      #_(ctf/dump-tree file' (thf/current-page-id file') {(:id file') file'} {:show-ids true}) ;; Uncomment to debug
      (t/is (= expected-diff diff))))

  (t/testing "when components are in external libraries, the fix still works well"
    (let [library1
          ;; {:main1-root} [:name Frame1]              # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]              # [Component :component2]
          ;;     :nested2-head [:name Frame4]          @--> [Component :component4] :main4-root
          ;;                                                {swap-slot :nested2-head}
          ;;         :nested4-head [:name Frame3]      @--> [Component: component3] :main3-root
          ;;             :nested4-child [:name Rect3]  ---> :nested4-child
          (-> (thf/sample-file :library1)
              (tho/add-nested-component :component1 :main1-root :main1-child
                                        :component2 :main2-root :nested2-head
                                        :nested-head-params {:children-labels [:nested2-child]}))
          library2
          ;; {:main3-root} [:name Frame3]           # [Component :component3]
          ;;     :main3-child [:name Rect3]
          ;;
          ;; {:main4-root} [:name Frame4]           # [Component :component4]
          ;;     :nested4-head [:name Frame3]       @--> [Component :component1] :main3-root
          ;;         :nested4-child [:name Rect3]   ---> :main3-child
          (-> (thf/sample-file :library2)
              (tho/add-nested-component :component3 :main3-root :main3-child
                                        :component4 :main4-root :nested4-head
                                        :root1-params {:name "Frame3"}
                                        :main1-child-params {:name "Rect3"}
                                        :main2-root-params {:name "Frame4"}
                                        :nested-head-params {:children-labels [:nested4-child]}))

          library1
          (tho/swap-component-in-shape library1 :nested2-head :component4 :library library2)

          file
          ;; :copy2 [:name Frame2]                  #--> [Component :component2] :main2-root
          ;;     :copy2-nested-head [:name Frame4]  @--> [Component <bad>] :main4-root   ## <- BAD component-id/file
          ;;         <no-label> [:name Frame3]      @--> :nested4-head
          ;;             <no-label> [:name Rect3]   ---> :nested4-child
          (-> (thf/sample-file :file1)
              (thc/instantiate-component :component2 :copy2 :children-labels [:copy2-nested-head]
                                         :library library1)
              (ths/update-shape :copy2-nested-head :component-id (thi/new-id! :some-other-id))
              (ths/update-shape :copy2-nested-head :component-file (thi/new-id! :some-other-file)))

          libraries {(:id library1) library1
                     (:id library2) library2}

          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % libraries))

          diff (d/map-diff file file')

          expected-diff {:data
                         {:pages-index
                          {(thf/current-page-id file)
                           {:objects
                            {(thi/id :copy2-nested-head)
                             {:component-id
                              [(thi/id :some-other-id) (thi/id :component4)]
                              :component-file
                              [(thi/id :some-other-file) (thi/id :library2)]}}}}}}]

      #_(thf/dump-file library2) ;; Uncomment to debug
      (t/is (= expected-diff diff))))

  (t/testing "file with several broken ids should propagate to all copies"
    (let [file
          ;; {:main1-root} [:name Frame1]            # [Component :component1]
          ;;     :main1-child [:name Rect1]
          ;;
          ;; {:main2-root} [:name Frame2]            # [Component :component2]
          ;;     :nested-head1 [:name Frame1]        @--> [Component :component1] :main1-root
          ;;         <no-label> [:name Rect1]        ---> :main1-child
          ;;
          ;; {:main3-root} [:name Frame3]            # [Component :component3]
          ;;     :nested-head2 [:name Frame2]        @--> [Component <bad>] :main2-root    ## <- BAD component-id
          ;;         :nested-subhead2 [:name Frame1] @--> [Component <bad>] :nested-head1  ## <- BAD component-id
          ;;             <no-label> [:name Rect1]    ---> <no-label>
          ;;
          ;; :copy2-root [:name Frame3]              #--> [Component :component3] :main3-root
          ;;     <no-label> [:name Frame2]           @--> [Component :component2] :nested-head2
          ;;         <no-label> [:name Frame1]       @--> [Component :component1] :nested-subhead2
          ;;             <no-label> [:name Rect1]    ---> <no-label>
          (-> (thf/sample-file :file1)
              (tho/add-two-levels-nested-component-with-copy :component1 :main1-root :main1-child
                                                             :component2 :main2-root :nested-head1
                                                             :component3 :main3-root :nested-head2 :nested-subhead2
                                                             :copy2-root)
              ;; Corrupt both levels
              (ths/update-shape :nested-head2 :component-id (thi/new-id! :wrong-comp2))
              (ths/update-shape :nested-subhead2 :component-id (thi/new-id! :wrong-comp3)))

          file' (ctf/update-file-data file #(cfcp/sync-component-id-with-ref-shape % {}))
          copy2-root (ths/get-shape file' :copy2-root)
          copy2-root-child1 (ths/get-shape-by-id file' (first (:shapes copy2-root)))
          copy2-root-child2 (ths/get-shape-by-id file' (first (:shapes copy2-root-child1)))

          diff (d/map-diff file file')

          expected-diff {:data
                         {:pages-index
                          {(thf/current-page-id file)
                           {:objects
                            {(:id copy2-root-child1)
                             {:component-id [(thi/id :component2) (thi/id :wrong-comp2)]}
                             (:id copy2-root-child2)
                             {:component-id [(thi/id :component1) (thi/id :wrong-comp3)]}}}}}}]

      (thf/dump-file file') ;; Uncomment to debug
      (t/is (= expected-diff diff)))))

