;;; treemacs-test.el --- Tests for treemacs -*- lexical-binding: t -*-

;; Copyright (C) 2024 Alexander Miller

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;;; Code:

(require 'filenotify)
(require 'dash)
(require 'pfuture)
(require 'treemacs)
(require 'treemacs-macros)
(require 'treemacs-bookmarks)
(require 'treemacs-core-utils)
(require 'treemacs-tags)
(require 'treemacs-tag-follow-mode)
(require 'treemacs-mouse-interface)
(require 'treemacs-file-management)
(require 'org)
(require 'buttercup)

(defconst treemacs-should-run-file-notify-tests (not (null file-notify--library)))

(defmacro treemacs--with-project (pr &rest body)
  "Set PR as the only project in current workspace and then run BODY."
  (declare (indent 1))
  `(let ((--original-- (treemacs-current-workspace))
         (ws (treemacs-workspace->create! :name "FAKE" :projects ,(when pr `(list ,pr)))))
     (unwind-protect
         (progn
           (setf (treemacs-current-workspace) ws)
           ,@body)
       (progn
         (setf (treemacs-current-workspace) --original--)))))

(defmacro treemacs--save-workspace (&rest body)
  "Execute BODY saving the current workspace."
  `(-let [ws (treemacs-current-workspace)]
     (unwind-protect
         ,@body
       (setf (treemacs-current-workspace) ws))))

(describe "treemacs-is-path"

  (describe ":in matcher"

    (it "identifies direct parent"
      (let ((path "~/A/B/c")
            (parent "~/A/B"))
        (expect (treemacs-is-path path :in parent) :to-be-truthy)))

    (it "identifies indirect parent"
      (let ((path "~/A/B/C/D/e")
            (parent "~/A/B"))
        (expect (treemacs-is-path path :in parent) :to-be-truthy)))

    (it "identifies non-parent"
      (let ((path "~/A/B/C/D/e")
            (parent "~/B"))
        (expect (treemacs-is-path path :in parent) :not :to-be-truthy)))

    (it "identifies non-parent with similar prefix"
      (let ((path "~/A/prefix1")
            (parent "~/A/prefix2"))
        (expect (treemacs-is-path path :in parent) :not :to-be-truthy))))

  (describe ":directly-in matcher"

    (it "identifies direct parent"
      (let ((path "~/A/B/c")
            (parent "~/A/B"))
        (expect (treemacs-is-path path :directly-in parent) :to-be-truthy)))

    (it "rejects indirect parent"
      (let ((path "~/A/B/c")
            (parent "~/A"))
        (expect (treemacs-is-path path :directly-in parent) :to-be nil)))

    (it "rejects non-parent"
      (let ((path "~/A/B/c")
            (parent "x"))
        (expect (treemacs-is-path path :directly-in parent) :to-be nil)))

    (it "rejects non-parent with same length as parent"
      (let ((path "~/A/B/c")
            (parent "~/A/X"))
        (expect (treemacs-is-path path :directly-in parent) :to-be nil)))

    (it "rejects non-parent with similar preix"
      (let ((path "~/A/prefix1")
            (parent "~/A/prefix2"))
        (expect (treemacs-is-path path :directly-in parent) :to-be nil)))

    (it "rejects shorter path than parent"
      (let ((path "~/A")
            (parent "~/A/B/C"))
        (expect (treemacs-is-path path :directly-in parent) :to-be nil))))

  (describe ":in-project matcher"

    (it "Identifies that a path is in a project"
      (let ((path "~/P/A/B/C/D/E/F/file")
            (project (treemacs-project->create! :name "P" :path "~/P/A/B/C" :path-status 'local-readable)))
        (expect (treemacs-is-path path :in-project project) :to-be-truthy)))

       (it "Identifies that a path is not in a project"
         (let ((path "~/X/abc")
               (project (treemacs-project->create! :name "P" :path "~/P" :path-status 'local-readable)))
           (expect (treemacs-is-path path :in-project project) :not :to-be-truthy))))

  (describe ":in-workspace matcher"

    (it "Finds project of path in the workspace"
      (let* ((path "~/C/abc")
             (p1 (treemacs-project->create! :name "P1" :path "~/A" :path-status 'local-readable))
             (p2 (treemacs-project->create! :name "P2" :path "~/B" :path-status 'local-readable))
             (p3 (treemacs-project->create! :name "P3" :path "~/C" :path-status 'local-readable))
             (ws (treemacs-workspace->create! :name "WS" :projects (list p1 p2 p3))))
        (expect (treemacs-is-path path :in-workspace ws) :to-be p3)))

    (it "Identifies path not in the workspace" ()
        (let* ((path "~/D/abc")
               (p1 (treemacs-project->create! :name "P1" :path "~/A" :path-status 'local-readable))
               (p2 (treemacs-project->create! :name "P2" :path "~/B" :path-status 'local-readable))
               (p3 (treemacs-project->create! :name "P3" :path "~/C" :path-status 'local-readable))
               (ws (treemacs-workspace->create! :name "WS" :projects (list p1 p2 p3))))
          (expect (treemacs-is-path path :in-workspace ws) :to-be nil)))))

(describe "treemacs--reject-ignored-files"

  (let ((treemacs-ignored-file-predicates (default-value 'treemacs-ignored-file-predicates)))

    (describe "Accepting"

      (it "Accepts dot-file"
        (expect (treemacs--reject-ignored-files "~/A/B/C/.foo.el") :to-be t))

      (it "Accepts common absolute path"
        (expect (treemacs--reject-ignored-files "~/A/B/C/foo.el") :to-be t))

      (it "Accepts common filename"
        (expect (treemacs--reject-ignored-files "foo.el") :to-be t))

      (it "Accepts directory"
        (expect (treemacs--reject-ignored-files "~/A/B/C/") :to-be t))

      (it "Accepts .git when it is not hidden"
        (-let [treemacs-hide-dot-git-directory nil]
          (expect (treemacs--reject-ignored-files "~/A/B/C/.git") :to-be t))))

    (describe "Rejecting"

      (it "Fails on nil input"
        (expect (treemacs--reject-ignored-files nil) :to-throw))

      (it "Fails on empty name"
        (expect (treemacs--reject-ignored-files "") :to-throw))

      (it "Rejects Emacs lock file"
        (expect (treemacs--reject-ignored-files "~/A/B/C/.#foo.el") :to-be nil))

      (it "Rejects Emacs backup file"
        (expect (treemacs--reject-ignored-files "~/A/B/C/foo.el~") :to-be nil))

      (it "Rejects autosave file"
        (expect (treemacs--reject-ignored-files "~/A/B/C/#foo.el#") :to-be nil))

      (it "Rejects flycheck's temp file"
        (expect (treemacs--reject-ignored-files "~/A/B/C/flycheck_foo.el") :to-be nil))

      (it "Rejects dot"
        (expect (treemacs--reject-ignored-files ".") :to-be nil))

      (it "Rejects dot-dot"
        (expect (treemacs--reject-ignored-files "..") :to-be nil)))))

(describe "treemacs--reject-ignored-and-dotfiles"

  (let ((treemacs-ignored-file-predicates (default-value 'treemacs-ignored-file-predicates)))

    (describe "Accepting"

      (it "Accepts common absolute path"
        (expect (treemacs--reject-ignored-and-dotfiles "~/A/B/C/foo.el") :to-be t))

      (it "Accepts common filename"
        (expect (treemacs--reject-ignored-and-dotfiles "foo.el") :to-be t))

      (it "Accepts directory"
        (expect (treemacs--reject-ignored-and-dotfiles "~/A/B/C/") :to-be t)))

    (describe "Rejecting"

      (it "Fails on nil input"
        (expect (treemacs--reject-ignored-and-dotfiles nil) :to-throw))

      (it "Fails on empty name"
        (expect (treemacs--reject-ignored-and-dotfiles "") :to-throw))

      (it "Rejects Emacs lock file"
        (expect (treemacs--reject-ignored-and-dotfiles "~/A/B/C/.#foo.el") :to-be nil))

      (it "Rejects Emacs backup file"
        (expect (treemacs--reject-ignored-and-dotfiles "~/A/B/C/foo.el~") :to-be nil))

      (it "Rejects autosave file"
        (expect (treemacs--reject-ignored-and-dotfiles "~/A/B/C/#foo.el#") :to-be nil))

      (it "Rejects flycheck's temp file"
        (expect (treemacs--reject-ignored-and-dotfiles "~/A/B/C/flycheck_foo.el") :to-be nil))

      (it "Rejects dot-file"
        (expect (treemacs--reject-ignored-and-dotfiles "~/A/B/C/.foo.el") :to-be nil))

      (it "Rejects .git"
        (expect (treemacs--reject-ignored-and-dotfiles "~/A/B/C/.git") :to-be nil))

      (it "Rejects dot"
        (expect (treemacs--reject-ignored-and-dotfiles ".") :to-be nil))

      (it "Rejects dot-dot"
          (expect (treemacs--reject-ignored-and-dotfiles "..") :to-be nil)))))

(describe "treemacs--is-event-relevant?"

  (-let [treemacs-ignored-file-predicates (default-value 'treemacs-ignored-file-predicates)]

    (describe "accept"

      (it "accepts change event when git-mode is enabled"
        (let ((treemacs-git-mode t)
              (event '(nil changed "~/A/a")))
          (expect (treemacs--is-event-relevant? event) :to-be-truthy)))

      (it "accepts create events"
        (-let [event '(nil created "~/A/a")]
          (expect (treemacs--is-event-relevant? event) :to-be-truthy)))

      (it "accepts delete events"
        (-let [event '(nil deleted "~/A/a")]
          (expect (treemacs--is-event-relevant? event) :to-be-truthy))))

    (describe "reject"

      (it "rejects stop-watch event"
        (-let [event '(nil stopped "~/A/a")]
          (expect (treemacs--is-event-relevant? event) :not :to-be-truthy)))

      (it "rejects change event when git-mode is disabled"
        (let ((treemacs-git-mode nil)
              (event '(nil changed "~/A/a")))
          (expect (treemacs--is-event-relevant? event) :not :to-be-truthy)))

      (it "rejects lockfile events"
        (-let [event '(nil created "~/A/.#foo.el")]
          (expect (treemacs--is-event-relevant? event) :not :to-be-truthy)))

      (it "rejects flycheck file events"
        (-let [event '(nil created "~/A/flycheck_foo.el")]
          (expect (treemacs--is-event-relevant? event) :not :to-be-truthy))))))

(describe "treemacs--file-extension"

  (it "Fails on nil input"
    (expect (treemacs--file-extension nil) :to-throw))

  (it "Returns empty string when input is empty string"
    (expect (treemacs--file-extension "") :to-equal ""))

  (it "Returns empty string when input is only period"
    (expect (treemacs--file-extension ".") :to-equal ""))

  (it "Returns empty string when input is many periods"
    (expect (treemacs--file-extension ".......") :to-equal ""))

  (it "Returns input for an absolute path without extension"
    (expect (treemacs--file-extension "/A/B/C/D/foo") :to-equal "/A/B/C/D/foo"))

  (it "Returns the filename of a filename without extension"
    (expect (treemacs--file-extension "foo") :to-equal "foo"))

  (it "Returns the extension of an absolute path"
    (expect (treemacs--file-extension "~/A/B/C/D/foo.el") :to-equal "el"))

  (it "Returns the extension of a filename"
    (expect (treemacs--file-extension "foo.el") :to-equal "el"))

  (it "Returns the extension of absolute path with periods"
    (expect (treemacs--file-extension "~/A/foo.bar/baz.qux/foo.el") :to-equal "el")))

(describe "treemacs--partition-imenu-index"

  (it "Returns nil on nil input"
    (expect (treemacs--partition-imenu-index nil "A") :not :to-be-truthy))

  (it "Returns index unchanged when input has no top level leaves"
    (expect '(("A" ("a1" "a2")) ("B" ("b1" "b2")))
            :to-equal
            (treemacs--partition-imenu-index '(("A" ("a1" "a2")) ("B" ("b1" "b2"))) "Functions")))

  (it "Partitions single top-level list into Functions"
    (expect '(("Functions" ("x" "y" "z")))
            :to-equal
            (treemacs--partition-imenu-index '(("x" "y" "z")) "Functions")))

  (it "Partitions top-level lists into Functions"
    (expect '(("A" ("a1" "a2")) ("B" ("b1" "b2")) ("Functions" ("x" "y" "z")))
            :to-equal
            (treemacs--partition-imenu-index '(("A" ("a1" "a2")) ("B" ("b1" "b2")) ("x" "y" "z")) "Functions"))))

(describe "treemacs--should-reenter?"

  (describe "Accepting"

    (it "Accepts nil"
      (expect (treemacs--should-reenter? nil)
              :to-be t))

    (it "Accepts custom nodes"
      (expect (treemacs--should-reenter? '(:custom A B))
              :to-be t))

    (it "Accepts non-string/cons paths"
      (expect (treemacs--should-reenter? 'X)
              :to-be t))

    (it "Accepts non-dotfiles"
      (-let [treemacs-show-hidden-files nil]
        (expect (treemacs--should-reenter? "/foo/bar")
                :to-be t)))

    (it "Accepts extensions under non-dotfiles"
      (-let [treemacs-show-hidden-files nil]
        (expect (treemacs--should-reenter? '("/foo/bar" A B))
                :to-be t)))

    (it "Accepts dotfiles when they are shown"
      (-let [treemacs-show-hidden-files t]
        (expect (treemacs--should-reenter? "/foo/.bar")
                :to-be t)))

    (it "Accepts extensions under dotfiles when they are shown"
      (-let [treemacs-show-hidden-files t]
        (expect (treemacs--should-reenter? '("/foo/.bar" A B))
                :to-be t))))

  (describe "Rejecting"

    (it "Rejects dotfiles when they are hidden"
      (-let [treemacs-show-hidden-files nil]
        (expect (treemacs--should-reenter? "/foo/.bar")
                :to-be nil)))

    (it "Rejects extensions under dotfiles when they are hidden"
      (-let [treemacs-show-hidden-files nil]
        (expect (treemacs--should-reenter? '("/foo/.bar" A B))
                :to-be nil)))))

(describe "treemacs--parent"

  (it "Does not fail for nil input"
    (expect (treemacs--parent nil) :to-be nil))

  (it "Returns nil when input is empty"
    (expect (treemacs--parent "") :to-be nil))

  (it "Returns nil when input is not a valid path"
    (expect (treemacs--parent "ABC") :to-be nil))

  (it "Correctly identifies a parent path"
    (expect (treemacs--parent "/home/A/B") :to-equal "/home/A"))

  (it "Returns the system root when it's the input"
    (expect (treemacs--parent "/") :to-equal "/"))

  (it "Returns parent of root-level extension node."
    (expect (treemacs--parent '(a b)) :to-equal '(a)))

  (it "Returns parent of directory extension node."
    (expect (treemacs--parent '("/test1" "a" "b")) :to-equal '("/test1" "a"))))

(describe "treemacs--get-or-parse-git-result"

  (it "Returns an empty table when input is nil"
    (-let [result (treemacs--get-or-parse-git-result nil)]
      (expect result :to-be-truthy)
      (expect (ht-empty? result) :to-be t)))

  (it "Returns an already parsed table"
    (let ((input (pfuture-new "echo"))
          (result (ht)))
      (process-put input 'git-table result)
      (expect (treemacs--get-or-parse-git-result input) :to-be result)))

  (it "Parses a process' git output"

    (spy-on #'treemacs--git-status-parse-function
            :and-return-value (ht ("A" 1) ("B" 2)))

     (let* ((input (pfuture-new "echo"))
            (result (treemacs--get-or-parse-git-result input)))
       (expect (ht? result))
       (expect (= 2 (ht-size result)))
       (expect (= 1 (ht-get result "A")))
       (expect (= 2 (ht-get result "B")))
       (expect #'treemacs--git-status-parse-function :to-have-been-called))))

(describe "treemacs--on-rename"

  (it "Does nothing when the dom is empty"
    (with-temp-buffer
      (-let [treemacs-dom (ht)]
        (treemacs--on-rename "OLD" "NEW" nil)
        (expect (ht-empty? treemacs-dom) :to-be t))))

  (it "Does nothing when the old key is not in the dom"
    (with-temp-buffer
      (-let [treemacs-dom (ht ("A" (treemacs-dom-node->create! :key "A")))]
        (treemacs--on-rename "OLD" "NEW" nil)
        (expect (ht-size treemacs-dom) :to-equal 1)
        (expect (ht-get treemacs-dom "A") :to-be-truthy))))

  (it "Correctly renamed a full subtree"
    (with-temp-buffer
      (let* ((default-directory "/A")
             (root (treemacs-dom-node->create! :key "/A"))
             (node1 (treemacs-dom-node->create! :key "/A/OLD"))
             (node2 (treemacs-dom-node->create! :key "/A/OLD/X"))
             (node3 (treemacs-dom-node->create! :key "/A/OLD/X/Y"))
             (node4 (treemacs-dom-node->create! :key (list "/A/OLD/X/Y" "Classes")))
             (node5 (treemacs-dom-node->create! :key (list "/A/OLD/X/Y" "Classes" "Class Foo")))
             (node6 (treemacs-dom-node->create! :key (list "/A/OLD/X/Y" "Classes" "Class Foo" "void bar()")))
             (nodex (treemacs-dom-node->create! :key "/A/B"))
             (nodey (treemacs-dom-node->create! :key "/A/B/C")))
        (setf (treemacs-dom-node->parent nodex) root
              (treemacs-dom-node->parent nodey) root
              (treemacs-dom-node->parent node1) root
              (treemacs-dom-node->parent node2) node1
              (treemacs-dom-node->parent node3) node2
              (treemacs-dom-node->parent node4) node3
              (treemacs-dom-node->parent node5) node4
              (treemacs-dom-node->parent node6) node5
              (treemacs-dom-node->children root) (list node1 nodex nodey)
              (treemacs-dom-node->children node1) (list node2)
              (treemacs-dom-node->children node2) (list node3)
              (treemacs-dom-node->children node3) (list node4)
              (treemacs-dom-node->children node4) (list node5)
              (treemacs-dom-node->children node5) (list node6))
        (setq treemacs-dom
              (ht ((treemacs-dom-node->key root) root)
                  ((treemacs-dom-node->key nodex) nodex)
                  ((treemacs-dom-node->key nodey) nodey)
                  ((treemacs-dom-node->key node1) node1)
                  ((treemacs-dom-node->key node2) node2)
                  ((treemacs-dom-node->key node3) node3)
                  ((treemacs-dom-node->key node4) node4)
                  ((treemacs-dom-node->key node5) node5)
                  ((treemacs-dom-node->key node6) node6)))
        (treemacs--on-rename "/A/OLD" "/A/NEW" nil)
        (dolist (key '("/A/OLD"
                       "/A/OLD/X"
                       "/A/OLD/X/Y"
                       ("/A/OLD/X/Y" "Classes")
                       ("/A/OLD/X/Y" "Classes" "Class Foo")
                       ("/A/OLD/X/Y" "Classes" "Class Foo" "void bar()")))
          (expect (ht-get treemacs-dom key) :to-be nil))
        (dolist (key '("/A/NEW" "/A/NEW/X" "/A/NEW/X/Y"
                       ("/A/NEW/X/Y" "Classes")
                       ("/A/NEW/X/Y" "Classes" "Class Foo")
                       ("/A/NEW/X/Y" "Classes" "Class Foo" "void bar()")))
          (expect (ht-get treemacs-dom key) :to-be-truthy))
        (expect (ht-size treemacs-dom) :to-equal 9))))

 (it "Won't rename initial node when filewatch is enabled"
    (with-temp-buffer
      (let* ((default-directory "/A")
             (root (treemacs-dom-node->create! :key "/A"))
             (node1 (treemacs-dom-node->create! :key "/A/OLD"))
             (node2 (treemacs-dom-node->create! :key "/A/OLD/X"))
             (node3 (treemacs-dom-node->create! :key "/A/OLD/X/Y"))
             (nodex (treemacs-dom-node->create! :key "/A/B"))
             (nodey (treemacs-dom-node->create! :key "/A/B/C")))
        (setf (treemacs-dom-node->parent nodex) root
              (treemacs-dom-node->parent nodey) root
              (treemacs-dom-node->parent node1) root
              (treemacs-dom-node->parent node2) node1
              (treemacs-dom-node->parent node3) node2
              (treemacs-dom-node->children root) (list node1 nodex nodey)
              (treemacs-dom-node->children node1) (list node2)
              (treemacs-dom-node->children node2) (list node3))
        (setf treemacs-dom
              (ht ((treemacs-dom-node->key root) root)
                  ((treemacs-dom-node->key nodex) nodex)
                  ((treemacs-dom-node->key nodey) nodey)
                  ((treemacs-dom-node->key node1) node1)
                  ((treemacs-dom-node->key node2) node2)
                  ((treemacs-dom-node->key node3) node3)))
        (treemacs--on-rename "/A/OLD" "/A/NEW" t)
        (dolist (key '("/A/OLD/X" "/A/OLD/X/Y"))
          (expect (ht-get treemacs-dom key) :to-be nil))
        (dolist (key '("/A/OLD" "/A/NEW/X" "/A/NEW/X/Y"))
          (expect (ht-get treemacs-dom key) :to-be-truthy))
        (expect (ht-size treemacs-dom) :to-equal 6)))))

(describe "treemacs-on-collapse"

  (it "Fails when key is nil"
    (with-temp-buffer
      (-let [treemacs-dom (ht)]
        (expect (treemacs-on-collapse nil) :to-throw))))

  (it "Removes empty nodes from reentry"
    (with-temp-buffer
      (let* ((default-directory "/A")
             (treemacs-dom (ht))
             (root (progn
                     (ht-set! treemacs-dom default-directory
                              (treemacs-dom-node->create! :key default-directory))
                     (treemacs-find-in-dom default-directory)))
             (node (progn
                     (ht-set! treemacs-dom "/A/B"
                              (treemacs-dom-node->create! :key "/A/B"))
                     (treemacs-find-in-dom "/A/B"))))
        (setf (treemacs-dom-node->parent node) root
              (treemacs-dom-node->children root) (list node)
              (treemacs-dom-node->reentry-nodes root) (list node))
        (treemacs-on-collapse "/A/B")
        (expect (treemacs-dom-node->reentry-nodes root) :to-be nil))))

  (it "Keeps node with children in reentry"
    (with-temp-buffer
      (let* ((default-directory "/A")
             (treemacs-dom (ht))
             (root (progn
                     (ht-set! treemacs-dom default-directory
                              (treemacs-dom-node->create! :key default-directory))
                     (treemacs-find-in-dom default-directory)))
             (node1 (progn
                      (ht-set! treemacs-dom "/A/B"
                               (treemacs-dom-node->create! :key "/A/B"))
                      (treemacs-find-in-dom "/A/B")))
             (node2 (progn
                      (ht-set! treemacs-dom "/A/B/C"
                               (treemacs-dom-node->create! :key "/A/B/C"))
                      (treemacs-find-in-dom "/A/B/C"))))
        (setf (treemacs-dom-node->parent node1) root
              (treemacs-dom-node->parent node2) node1
              (treemacs-dom-node->children root) (list node1)
              (treemacs-dom-node->children node1) (list node2)
              (treemacs-dom-node->reentry-nodes root) (list node1)
              (treemacs-dom-node->reentry-nodes node1) (list node2))
        (treemacs-on-collapse "/A/B")
        (expect (ht-size treemacs-dom)
                :to-equal 2)
        (expect (treemacs-dom-node->children root)
                :to-equal (list node1))
        (expect (treemacs-dom-node->children node1)
                :to-be nil)
        (expect (treemacs-find-in-dom "/A/B/C")
                :to-be nil)
        (expect (treemacs-dom-node->reentry-nodes root)
                :to-be nil)
        (expect (treemacs-dom-node->reentry-nodes node1)
                :to-equal (list node2)))))

  (it "Removes a subtree when purging"
    (with-temp-buffer
      (let* ((default-directory "/A")
             (treemacs-dom (ht))
             (root (progn
                     (ht-set! treemacs-dom default-directory
                              (treemacs-dom-node->create! :key default-directory))
                     (treemacs-find-in-dom default-directory)))
             (node1 (progn
                      (ht-set! treemacs-dom "/A/B"
                               (treemacs-dom-node->create! :key "/A/B"))
                      (treemacs-find-in-dom "/A/B")))
             (node2 (progn
                      (ht-set! treemacs-dom "/A/B/C" (treemacs-dom-node->create! :key "/A/B/C"))
                      (treemacs-find-in-dom "/A/B/C"))))
        (setf (treemacs-dom-node->parent node1) root
              (treemacs-dom-node->parent node2) node1
              (treemacs-dom-node->children root) (list node1)
              (treemacs-dom-node->children node1) (list node2)
              (treemacs-dom-node->reentry-nodes root) (list node1)
              (treemacs-dom-node->reentry-nodes node1) (list node2))
        (treemacs-on-collapse "/A/B" :purge)
        (expect (ht-size treemacs-dom)
                :to-equal 2)
        (expect (ht-get treemacs-dom "/A/B/C")
                :to-be nil)
        (expect (treemacs-dom-node->children node1)
                :to-be nil)
        (expect (treemacs-dom-node->reentry-nodes node1)
                :to-be nil)))))

(describe "treemacs-on-expand"

  (it "Fails when key is nil"
    (with-temp-buffer
      (-let [treemacs-dom (ht)]
        (expect (treemacs-on-expand nil 1) :to-be nil))))

  (it "Correctly expands root node"
    (with-temp-buffer
      (let* ((default-directory "/A")
             (treemacs-dom (ht)))
        (treemacs-on-expand "/A" 1)
        (-let [root (treemacs-find-in-dom default-directory)]
          (expect (ht-size treemacs-dom) :to-equal 1)
          (expect (treemacs-dom-node->position root) :to-equal 1)
          (expect (treemacs-dom-node->parent root) :to-be nil)))))

  (it "Correctly expands child node"
    (with-temp-buffer
      (let* ((default-directory "/A")
             (treemacs-dom (ht))
             (root (progn
                     (ht-set! treemacs-dom default-directory
                              (treemacs-dom-node->create! :key default-directory))
                     (treemacs-find-in-dom default-directory)))
             (node (progn
                     (ht-set! treemacs-dom "/A/B"
                              (treemacs-dom-node->create! :key "/A/B"))
                     (treemacs-find-in-dom "/A/B"))))
        (setf (treemacs-dom-node->parent node) root
              (treemacs-dom-node->children root) (list node))
        (treemacs-on-expand "/A/B" 2)
        (expect (treemacs-dom-node->position node) :to-equal 2)
        (expect (treemacs-dom-node->reentry-nodes root) :to-equal (list node))))))

(when treemacs-should-run-file-notify-tests

  (describe "treemacs--start-watching"

    (before-each
      (spy-on #'file-notify-add-watch :and-return-value 123456))

    (it "Stars watching an unwatched file"
      (let ((path "/A")
            (treemacs-filewatch-mode t)
            (treemacs--filewatch-index (make-hash-table :test #'equal))
            (treemacs--collapsed-filewatch-index (make-hash-table :test #'equal)))
        (treemacs--start-watching path t)
        (expect (gethash path treemacs--filewatch-index)
                :to-equal
                (cons (list (current-buffer)) 123456))
        (expect (gethash path treemacs--collapsed-filewatch-index) :to-be-truthy)
        (expect #'file-notify-add-watch :to-have-been-called)))

    (it "Keeps watching an already watched file"
      (let ((path "/A")
            (treemacs-filewatch-mode t)
            (treemacs--filewatch-index (make-hash-table :test #'equal))
            (treemacs--collapsed-filewatch-index (make-hash-table :test #'equal)))
        (puthash path (cons '(x y) 123456) treemacs--filewatch-index)
        (treemacs--start-watching path t)
        (expect (gethash path treemacs--filewatch-index)
                :to-equal
                (cons (list (current-buffer) 'x 'y) 123456))
        (expect (gethash path treemacs--collapsed-filewatch-index)
                :to-be-truthy)
        (expect #'file-notify-add-watch :not :to-have-been-called)))

    (it "Adds a watching buffer only once"
      (let ((path "/A")
            (treemacs-filewatch-mode t)
            (treemacs--filewatch-index (make-hash-table :test #'equal))
            (treemacs--collapsed-filewatch-index (make-hash-table :test #'equal)))
        (puthash path (cons '(x y) 123456) treemacs--filewatch-index)
        (treemacs--start-watching path t)
        (treemacs--start-watching path t)
        (expect (gethash path treemacs--filewatch-index)
                :to-equal
                (cons (list (current-buffer) 'x 'y) 123456))
        (expect (gethash path treemacs--collapsed-filewatch-index) :to-be-truthy)
        (expect #'file-notify-add-watch :not :to-have-been-called)))))

(when treemacs-should-run-file-notify-tests

  (describe "treemacs--stop-watching"

    (it "Does nothing when path is not watched"
        (let ((treemacs--filewatch-index (make-hash-table :test #'equal))
              (treemacs--collapsed-filewatch-index (make-hash-table :test #'equal)))
          (expect (treemacs--stop-watching "/A") :to-be nil)))

    (it "Stops the watch of the only watching buffer"
      (spy-on #'file-notify-rm-watch :and-return-value t)
      (let ((path "/A")
            (treemacs--filewatch-index (make-hash-table :test #'equal))
            (treemacs--collapsed-filewatch-index (make-hash-table :test #'equal)))
        (puthash path (cons (list (current-buffer)) 123456) treemacs--filewatch-index)
        (puthash path t treemacs--collapsed-filewatch-index)
        (treemacs--stop-watching path)
        (expect (gethash path treemacs--filewatch-index) :to-be nil)
        (expect (gethash path treemacs--collapsed-filewatch-index) :to-be nil)))

    (it "Stops the watch of one of several buffers"
      (spy-on #'file-notify-rm-watch)
      (let ((path "/A")
            (treemacs--filewatch-index (make-hash-table :test #'equal))
            (treemacs--collapsed-filewatch-index (make-hash-table :test #'equal)))
        (puthash path (cons (list 'x 'y (current-buffer)) 123456) treemacs--filewatch-index)
        (puthash path t treemacs--collapsed-filewatch-index)
        (treemacs--stop-watching path)
        (expect (gethash path treemacs--filewatch-index)
                :to-equal (cons '(x y) 123456))
        (expect (gethash path treemacs--collapsed-filewatch-index) :to-be-truthy)
        (expect #'file-notify-rm-watch :not :to-have-been-called)))

    (it "Stops the watch of path below stopped path"
        (spy-on #'file-notify-rm-watch :and-return-value t)
        (let ((path "/A/B")
              (treemacs--filewatch-index (make-hash-table :test #'equal))
              (treemacs--collapsed-filewatch-index (make-hash-table :test #'equal)))
          (puthash path (cons (list (current-buffer)) 123456) treemacs--filewatch-index)
          (puthash path t treemacs--collapsed-filewatch-index)
          (treemacs--stop-watching "/A")
          (expect (gethash path treemacs--filewatch-index) :to-be nil)
          (expect (gethash path treemacs--collapsed-filewatch-index) :to-be nil)))

    (it "Stops the watch of all watching buffers"
        (spy-on #'file-notify-rm-watch :and-return-value t)
        (let ((path "/A")
              (treemacs--filewatch-index (make-hash-table :test #'equal))
              (treemacs--collapsed-filewatch-index (make-hash-table :test #'equal)))
          (puthash path (cons '(x y z) 123456) treemacs--filewatch-index)
          (puthash path t treemacs--collapsed-filewatch-index)
          (treemacs--stop-watching path t)
          (expect (gethash path treemacs--filewatch-index) :to-be nil)
          (expect (gethash path treemacs--collapsed-filewatch-index) :to-be nil)))))

(describe "treemacs--flatten&sort-imenu-index"

  (it "Correctly transforms an org-mode index"
    (let ((org-imenu-depth 10)
          (temp-file (make-temp-file "Treemacs Test")))
      (ignore org-imenu-depth) ; for the compiler
      (unwind-protect
          (progn
            (find-file-noselect temp-file)
            (with-current-buffer (get-file-buffer temp-file)
              (insert "* H1\n")
              (insert "** H1.2\n")
              (insert "*** H1.2.3\n")
              (insert "* H2\n")
              (org-mode)
              (save-buffer)
              (expect (treemacs--flatten&sort-imenu-index)
                      :to-equal
                      `((("H1"     . ,(move-marker (make-marker) 1)))
                        (("H1.2"   . ,(move-marker (make-marker) 6)) "H1")
                        (("H1.2.3" . ,(move-marker (make-marker) 14)) "H1" "H1.2")
                        (("H2"     . ,(move-marker (make-marker) 25)))))))
        (kill-buffer (get-file-buffer temp-file))
        (delete-file temp-file)))))

(describe "treemacs--find-index-pos"

  (it "Fails when point is nil"
    (-let [pos nil]
      (expect (treemacs--find-index-pos pos '((("A" . (make-marker))))) :to-throw)))

  (it "Returns nil when index is nil"
    (expect (treemacs--find-index-pos 1 nil) :to-be nil))

  (it "Finds the correct position before the first marker"
    (let ((input `((("A" . ,(move-marker (make-marker) 10)))
                   (("B" . ,(move-marker (make-marker) 20)))
                   (("C" . ,(move-marker (make-marker) 30))))))
      (expect (treemacs--find-index-pos 1 input)
              :to-equal (car input))))

  (it "Finds the correct position after the last marker"
    (let ((input `((("A" . ,(move-marker (make-marker) 10)))
                   (("B" . ,(move-marker (make-marker) 20)))
                   (("C" . ,(move-marker (make-marker) 30))))))
      (expect (treemacs--find-index-pos 100 input)
              :to-equal (-last-item input))))

  (it "Finds an index using binary search"
    (spy-on #'treemacs--binary-index-search :and-call-through)
    (with-temp-buffer
      ;; make those markers viable
      (dotimes (_ 10) (insert "            \n"))
      (let ((input `((("A" . ,(move-marker (make-marker) 10)))
                     (("B" . ,(move-marker (make-marker) 20)))
                     (("C" . ,(move-marker (make-marker) 30)))
                     (("D" . ,(move-marker (make-marker) 40)))
                     (("E" . ,(move-marker (make-marker) 50)))
                     (("F" . ,(move-marker (make-marker) 60)))
                     (("G" . ,(move-marker (make-marker) 70)))
                     (("H" . ,(move-marker (make-marker) 80)))
                     (("I" . ,(move-marker (make-marker) 90))))))
        (expect (treemacs--find-index-pos 72 input)
                :to-equal (nth 6 input))
        (expect #'treemacs--binary-index-search :to-have-been-called)))))

(describe "treemacs--find-project-for-path"

  (it "Returns nil when input is nil"
    (treemacs--with-project (treemacs-project->create! :path "/A" :path-status 'local-readable)
      (expect (treemacs--find-project-for-path nil) :to-be nil)))

  (it "Returns nil when the workspace is empty"
    (treemacs--with-project nil
      (expect (treemacs--find-project-for-path "/A") :to-be nil)))

  (it "Returns nil when path does not fit any project"
    (treemacs--with-project (treemacs-project->create! :path "/A/B" :path-status 'local-readable)
      (expect (treemacs--find-project-for-path "/A/C") :to-be nil)))

  (it "Returns project when path fits"
    (-let [project (treemacs-project->create! :path "/A/B" :path-status 'local-readable)]
      (treemacs--with-project project
        (expect (treemacs--find-project-for-path "/A/B/C")
                :to-equal project)))))

(describe "treemacs--flatten-imenu-index"

  (it "Does nothing when input is nil"
    (expect (treemacs--flatten-imenu-index nil) :to-be nil))

  (it "Does nothing when input is empty"
    (expect (treemacs--flatten-imenu-index (list)) :to-be nil))

  (it "Correctly parses a single item"
    (expect (treemacs--flatten-imenu-index '("Functions")) :to-be nil))

  (it "Correctly parses full index"
    (-let [input `(("Functions" ("f1" . 1) ("f2" . 2))
                   ("Types"     ("t1" . 3) ("t2" . 4))
                   ("Classes"   ("c1" ("Members" ("m1" . 5) ("m2" . 6)))))]
      (expect (treemacs--flatten-imenu-index input)
              :to-equal
              `((("f2" . 2) "Functions")
                (("f1" . 1) "Functions")
                (("t2" . 4) "Types")
                (("t1" . 3) "Types")
                (("m2" . 6) "Classes" "c1" "Members")
                (("m1" . 5) "Classes" "c1" "Members"))))))

(describe "treemacs--flatten-org-mode-imenu-index"

  (it "Does nothing when input is nil"
    (expect (treemacs--flatten-org-mode-imenu-index nil) :to-be nil))

  (it "Does nothing when input is empty"
    (expect (treemacs--flatten-org-mode-imenu-index (list)) :to-be nil))

  (it "Correctly parses a single item"
    (expect (treemacs--flatten-org-mode-imenu-index '("Functions"))
            :to-equal '(("Functions"))))

  (it "Correctly parses full index"
    (-let [input `(("Functions" ("f1" . 1) ("f2" . 2))
                   ("Types"     ("t1" . 3) ("t2" . 4))
                   ("Classes"   ("c1" ("Members" ("m1" . 5) ("m2" . 6)))))]
           (expect (treemacs--flatten-org-mode-imenu-index input)
              :to-equal
              `(("Classes")
                ("Types")
                ("Functions")
                (("f2" . 2) "Functions")
                (("f1" . 1) "Functions")
                (("t2" . 4) "Types")
                (("t1" . 3) "Types")
                ("c1" "Classes")
                ("Members" "Classes" "c1")
                (("m2" . 6) "Classes" "c1" "Members")
                (("m1" . 5) "Classes" "c1" "Members"))))))

(describe "treemacs--next-non-child-button"

  (it "Does nothing when input is nil"
    (expect (treemacs--next-non-child-button nil) :to-be nil))

  (it "Returns nil when there is only a single button"
    (with-temp-buffer
      (-let [b (insert-text-button "b")]
        (expect (treemacs--next-non-child-button b) :to-be nil))))

  (it "Directly returns the next button"
    (with-temp-buffer
      (let ((b1 (insert-text-button "b1"))
            (b2 (insert-text-button "b2")))
        (button-put b1 :depth 1)
        (button-put b2 :depth 1)
        (expect (marker-position (treemacs--next-non-child-button b1))
                :to-equal b2))))

  (it "Searches through higher-depth buttons"
    (with-temp-buffer
      (let ((b1 (insert-text-button "b1"))
            (b2 (insert-text-button "b2"))
            (b3 (insert-text-button "b3"))
            (b4 (insert-text-button "b4"))
            (b5 (insert-text-button "b5"))
            (b6 (insert-text-button "b6")))
        (button-put b1 :depth 1)
        (button-put b2 :depth 2)
        (button-put b3 :depth 3)
        (button-put b4 :depth 4)
        (button-put b5 :depth 5)
        (button-put b6 :depth 1)
        (expect (marker-position (treemacs--next-non-child-button b1))
                :to-equal b6))))

  (it "Returns nil when there is no next non-child button"
    (with-temp-buffer
      (let ((b1 (insert-text-button "b1"))
            (b2 (insert-text-button "b2"))
            (b3 (insert-text-button "b3"))
            (b4 (insert-text-button "b4"))
            (b5 (insert-text-button "b5"))
            (b6 (insert-text-button "b6")))
        (button-put b1 :depth 1)
        (button-put b2 :depth 2)
        (button-put b3 :depth 3)
        (button-put b4 :depth 4)
        (button-put b5 :depth 5)
        (button-put b6 :depth 6)
        (expect (treemacs--next-non-child-button b1) :to-be nil)))))

(describe "treemacs--validate-persist-lines"

  (describe "Successes"

    (it "Succeeds on correctly formed input"
      (-let [lines '("* W1"
                     "** P1"
                     " - path :: a"
                     "** P2"
                     "- path :: b"
                     "* W2"
                     "** P3"
                     " - path :: c")]
        (expect (treemacs--validate-persist-lines lines) :to-be 'success)))

    (it "Succeeds with the same path in multiple workspaces"
      (-let [lines '("* W1"
                     "** P1"
                     " - path :: /A/B"
                     "* W2"
                     "** P2"
                     " - path :: /A/B")]
        (expect (treemacs--validate-persist-lines lines) :to-be 'success)))

    (it "Succeeds with non-connectable remotes"
      (let* ((treemacs--org-edit-buffer-name (buffer-name))
             (lines '("* W1"
                      "** P1"
                      " - path :: /ftp:anonymous@ftp.invalid:/test-path")))
        (expect (treemacs--validate-persist-lines lines) :to-be 'success)))

    (it "Succeeds with disabled projects"
      (-let [lines '("* W1"
                     "** COMMENT P1"
                     " - path :: a"
                     "** P2"
                     " - path :: b"
                     "** COMMENT P3"
                     " - path :: c")]
        (expect (treemacs--validate-persist-lines lines) :to-be 'success)))

    (it "Succeeds with disabled workspaces"
      (-let [lines '("* COMMENT W1"
                     "** P1"
                     " - path :: a"
                     "* W2"
                     "** P2"
                     " - path :: b")]
        (expect (treemacs--validate-persist-lines lines) :to-be 'success))))

  (describe "Errors"

    (it "Fails when first line is not a workspace name"
      (-let [lines '("X")]
        (expect (treemacs--validate-persist-lines lines)
                :to-equal '(error "X" "First item must be a workspace name"))))

    (it "Fails when line after workspace name is not a project name"
      (-let [lines '("* X"
                     "Y")]
        (expect (treemacs--validate-persist-lines lines)
                :to-equal '(error "Y" "Workspace name must be followed by project name"))))

    (it "Fails when line after project name is not a property"
      (-let [lines '("* X"
                     "** Y"
                     "Z")]
        (expect (treemacs--validate-persist-lines lines)
                :to-equal '(error "** Y" "Project name must be followed by path declaration"))))

    (it "Fails when line after path is not a project or workspace"
      (-let [lines '("* X"
                     "** Y"
                     " - path :: Z"
                     "A")]
        (expect (treemacs--validate-persist-lines lines)
                :to-equal '(error " - path :: Z" "Path property must be followed by the next workspace or project"))))

    (it "Fails when lines end at workspace name"
      (-let [lines '("* X")]
        (expect (treemacs--validate-persist-lines lines)
                :to-equal '(error "* X" "Cannot end with a project or workspace name"))))

    (it "Fails when lines end at project name"
      (-let [lines '("* X"
                     "** X")]
        (expect (treemacs--validate-persist-lines lines)
                :to-equal '(error "** X" "Cannot end with a project or workspace name"))))

    (it "Fails when input is empty"
      (expect (treemacs--validate-persist-lines nil)
              :to-equal '(error :start "Input is empty")))

    (it "Fails when path appears more than once"
      (-let [lines '("* W1"
                     "** P1"
                     " - path :: /A/B/C"
                     "** P2"
                     " - path :: /A/B/C/D")]
        (expect (treemacs--validate-persist-lines lines)
                :to-equal
                '(error " - path :: /A/B/C/D" "Path '/A/B/C/D' appears in the workspace more than once."))))

    (it "Fails when all projects are disabled"
      (-let [lines '("* W1"
                     "** COMMENT P1"
                     " - path :: a"
                     "** COMMENT P2"
                     " - path :: b"
                     "** COMMENT P3"
                     " - path :: c")]
        (expect (treemacs--validate-persist-lines lines)
                :to-equal '(error " - path :: c" "Workspace must contain at least 1 project that is not disabled."))))

    (it "Fails when all workspaces are disabled"
      (-let [lines '("* COMMENT W1"
                     "** P1"
                     " - path :: a"
                     "* COMMENT W2"
                     "** P2"
                     " - path :: b")]
        (expect (treemacs--validate-persist-lines lines)
                :to-equal '(error " - path :: b" "There must be at least 1 worspace that is not disabled."))))))

(describe "treemacs--read-persist-lines"

  (it "Ignores commentes"
    (expect (treemacs--read-persist-lines "#\n#\n#") :to-be nil))

  (it "Ignores blanks"
    (expect (treemacs--read-persist-lines "   \n \n \t \t   ") :to-be nil))

  (it "Reads everything else"
    (expect (treemacs--read-persist-lines
             (concat "#Foo: Bar\n" "\n" "* Workspace\n" "\t\n" "** Project\n" "#Comment\n" " - path :: /x\n"))
            :to-equal
            '("* Workspace" "** Project" " - path :: /x"))))

(describe "treemacs--read-workspaces"

  (before-each
    (spy-on #'treemacs--get-path-status :and-return-value 'local-readable))

  (it "Reads workspaces correctly"
    (let* ((list '("* WS 1"
                   "** P1"
                   " - path :: /a"
                   "** P2" " - path :: /b"
                   "* WS 2"
                   "** P3"
                   " - path :: /c"
                   "** P4"
                   " - path :: /d"))
           (iter (treemacs-iter->create! :list list))
           (result (treemacs--read-workspaces iter))
           (disabled-workspaces (car result))
           (workspaces (cadr result)))

      (expect (length disabled-workspaces) :to-be 0)
      (expect (length workspaces) :to-be 2)

      (let* ((ws1 (car workspaces))
             (ws2 (cadr workspaces))
             (ws1-projects (treemacs-workspace->projects ws1))
             (ws2-projects (treemacs-workspace->projects ws2)))

        (expect (treemacs-workspace->name ws1) :to-equal "WS 1")
        (expect (treemacs-workspace->name ws2) :to-equal "WS 2")
        (expect (length ws1-projects) :to-be 2)
        (expect (length ws2-projects) :to-be 2)

        (let ((pr1 (car  ws1-projects))
              (pr2 (cadr ws1-projects))
              (pr3 (car  ws2-projects))
              (pr4 (cadr ws2-projects)))

          (expect (treemacs-project->name pr1) :to-equal "P1")
          (expect (treemacs-project->name pr2) :to-equal "P2")
          (expect (treemacs-project->name pr3) :to-equal "P3")
          (expect (treemacs-project->name pr4) :to-equal "P4")
          (expect (treemacs-project->path pr1) :to-equal "/a")
          (expect (treemacs-project->path pr2) :to-equal "/b")
          (expect (treemacs-project->path pr3) :to-equal "/c")
          (expect (treemacs-project->path pr4) :to-equal "/d")))))

  (it "Reads disbled workspaces correctly"
    (let* ((list '("* COMMENT WS 1"
                   "** P1"
                   " - path :: /a"))
           (iter (treemacs-iter->create! :list list))
           (result (treemacs--read-workspaces iter))
           (disabled-workspaces (car result))
           (workspaces (cadr result)))

      (expect (length disabled-workspaces) :to-be 1)
      (expect (length workspaces) :to-be 0)

      (let* ((ws1 (car disabled-workspaces))
             (ws1-projects (treemacs-workspace->projects ws1)))

        (expect (treemacs-workspace->name ws1) :to-equal "WS 1")
        (expect (treemacs-workspace->is-disabled? ws1) :to-be t)
        (expect (length ws1-projects) :to-be 1)

        (let ((pr1 (car ws1-projects)))
          (expect (treemacs-project->name pr1) :to-equal "P1")
          (expect (treemacs-project->path pr1) :to-equal "/a")))))

  (it "Reads disabled projects"
    (let* ((list '("* WS 1"
                   "** COMMENT P1"
                   " - path :: /a"))
           (iter (treemacs-iter->create! :list list))
           (result (treemacs--read-workspaces iter))
           (workspaces (cadr result))
           (disabled-workspaces (car result)))

      (expect (length workspaces) :to-be 1)
      (expect (length disabled-workspaces) :to-be 0)

      (-let [project (-> workspaces (car) (treemacs-workspace->projects) (car))]
        (expect (treemacs-project->name project) :to-equal "P1")
        (expect (treemacs-project->is-disabled? project) :to-be t)))))

(describe "treemacs--git-status-process"

  (it "Does not call treemacs--git-status-process-function with non-local or unreadable paths"
    (dolist (status '(local-unreadable remote-readable remote-unreadable remote-disconnected extension))
      (spy-on 'treemacs--git-status-process-function)
      (-> treemacs-dir
          (treemacs-join-path "test")
          (treemacs--git-status-process (treemacs-project->create! :name "P" :path treemacs-dir :path-status status)))
      (expect 'treemacs--git-status-process-function
              :not :to-have-been-called)))

  (it "Calls treemacs--git-status-process-function with local readable path"
    (spy-on 'treemacs--git-status-process-function)
    (let ((path (treemacs-join-path treemacs-dir "test")))
      (treemacs--git-status-process path (treemacs-project->create! :name "P" :path treemacs-dir :path-status 'local-readable))
      (expect 'treemacs--git-status-process-function
              :to-have-been-called-with path))))

(describe "treemacs--process-collapsed-dirs"

  (it "Does nothing with non-local or unreadable paths"
    (-let [treemacs-collapse-dirs 3]
      (dolist (status '(local-unreadable remote-readable remote-unreadable remote-disconnected extension))
        (expect (-> treemacs-dir
                    (treemacs-join-path "test")
                    (treemacs--collapsed-dirs-process (treemacs-project->create! :name "P" :path treemacs-dir :path-status status)))
                :to-equal
                nil)))))

(describe "treemacs--parse-collapsed-dirs"

  (it "Finds dirs to flatten in test directory"
    (-let [treemacs-collapse-dirs 3]
      (expect (-> treemacs-dir
                  (treemacs-join-path "test")
                  (treemacs--collapsed-dirs-process
                   (treemacs-project->create!
                    :name "P"
                    :path treemacs-dir
                    :path-status 'local-readable))
                  (treemacs--parse-collapsed-dirs))
              :to-equal
              `(("/testdir2/testdir3"
                 ,(treemacs-join-path treemacs-dir "test/testdir1")
                 ,(treemacs-join-path treemacs-dir "test/testdir1/testdir2")
                 ,(treemacs-join-path treemacs-dir "test/testdir1/testdir2/testdir3"))))))

  (it "Returns nil when there is nothing to flatten"
    (-let [treemacs-collapse-dirs 3]
      (expect (-> treemacs-dir
                  (treemacs-join-path "test/testdir1/testdir2")
                  (treemacs--collapsed-dirs-process (treemacs-project->create! :name "P" :path treemacs-dir :path-status 'local-readable))
                  (treemacs--parse-collapsed-dirs))
              :to-be nil))))

(describe "treemacs--remove-trailing-newline"

  (it "Fails on nil input"
    (expect (treemacs--remove-trailing-newline nil)
            :to-throw))

  (it "Fails on empty string"
    (expect (treemacs--remove-trailing-newline "")
            :to-throw))

  (it "Does nothing when input does not need trimming"
    (-let [input "abc"]
      (expect (treemacs--remove-trailing-newline input) :to-equal input)))

  (it "Removes the last newline"
    (expect (treemacs--remove-trailing-newline "abc\n") :to-equal "abc"))

  (it "Removes only the last newline"
    (expect (treemacs--remove-trailing-newline "abc\n\n\n") :to-equal "abc\n\n")))

(describe "treemacs--add-trailing-slash"

  (it "Fails on nil input"
    (expect (treemacs--add-trailing-slash nil) :to-throw))

  (it "Fails on blank input"
    (expect (treemacs--add-trailing-slash "") :to-throw))

  (it "Does not add slash if one is already present"
    (expect (treemacs--add-trailing-slash "/ABC/") :to-equal "/ABC/"))

  (it "Adds a slash when there isn't one"
    (expect (treemacs--add-trailing-slash "/ABC") :to-equal "/ABC/")))

(describe "treemacs--is-name-invalid?"
  (it "detects nil"
    (expect (treemacs--is-name-invalid? nil) :to-be t))

  (it "detects an empty string"
    (expect (treemacs--is-name-invalid? "") :to-be t))

  (it "detects a blank string"
    (expect (treemacs--is-name-invalid? "      ") :to-be t))

  (it "detects a string with newlines"
    (expect (treemacs--is-name-invalid? "a\nb") :to-be t)))

(describe "treemacs--find-workspace"

  (it "Finds nothing when there are no workspaces"
    (treemacs--save-workspace
     (-let [treemacs--workspaces nil]
       (treemacs--find-workspace)
       (expect (treemacs-current-workspace) :to-be nil)))

    (treemacs--save-workspace
     (-let [treemacs--workspaces nil]
       (treemacs--find-workspace "X")
       (expect (treemacs-current-workspace) :to-be nil))))

  (it "Finds the first workspace when there is no current file"
    (treemacs--save-workspace
     (let* ((ws1 (treemacs-workspace->create! :name "A"))
            (ws2 (treemacs-workspace->create! :name "B"))
            (treemacs--workspaces (list ws1 ws2)))
       (treemacs--find-workspace)
       (expect (treemacs-current-workspace) :to-be ws1))))

  (it "Finds the first workspace when nothing fits the current file"
    (treemacs--save-workspace
     (let* ((p1  (treemacs-project->create! :name "P1" :path "P1"))
            (p2  (treemacs-project->create! :name "P2" :path "P2"))
            (ws1 (treemacs-workspace->create! :name "A" :projects (list p1)))
            (ws2 (treemacs-workspace->create! :name "B" :projects (list p2)))
            (treemacs--workspaces (list ws1 ws2)))
       (treemacs--find-workspace "X")
       (expect (treemacs-current-workspace) :to-be ws1))))

  (it "Finds workspace which contains current file"
    (treemacs--save-workspace
     (let* ((p1  (treemacs-project->create! :name "P1" :path "P1"))
            (p2  (treemacs-project->create! :name "P2" :path "/A"))
            (ws1 (treemacs-workspace->create! :name "A" :projects (list p1)))
            (ws2 (treemacs-workspace->create! :name "B" :projects (list p2)))
            (treemacs--workspaces (list ws1 ws2)))
       (treemacs--find-workspace "/A/B/C")
       (expect (treemacs-current-workspace) :to-be ws2)))))

(defmacro test-treemacs--with-sample-buffer (&rest body)
  "Evaluate BODY with some buttons defined.

In BODY, variable PROJECT is defined."
  (declare (indent 0))
  (let ((parent-marker (make-symbol "parent-marker")))
    `(with-temp-buffer
       (let ((project (treemacs-project->create! :name "Project" :path "/project"))
             (,parent-marker nil))
         (insert-text-button "Project"
                             :path "/project"
                             :state 'root-node-open
                             :depth 0
                             :project project)
         (setq ,parent-marker (copy-marker (line-beginning-position)))
         (insert "\n")
         (insert-text-button "directory"
                             :path "/project/directory"
                             :key "/project/directory"
                             :state 'dir-node-open
                             :parent ,parent-marker
                             :depth 1)
         (setq ,parent-marker (copy-marker (line-beginning-position)))
         (insert "\n")
         (insert-text-button "file.txt"
                             :path "/project/directory/file.txt"
                             :key "/project/directory/file.txt"
                             :state 'file-node-closed
                             :parent ,parent-marker
                             :depth 2)
         (setq ,parent-marker (copy-marker (line-beginning-position)))
         (insert "\n")
         (goto-char 0)
         ,@body))))

(defun test-treemacs--format-pattern (template expected-1 expected-2 expected-3)
  "Test that `treemacs--format-bookmark-title' expands TEMPLATE correctly.

EXPECTED-1 is the expected expansion of the \"Project\" button.
EXPECTED-2 is the expected expansion of the \"directory\" button.
EXPECTED-3 is the expected expansion of the \"file.txt\" button."
  (test-treemacs--with-sample-buffer
   (let ((treemacs-bookmark-title-template template))
     (expect (treemacs--format-bookmark-title (treemacs-current-button)) :to-equal expected-1)
     (forward-line 1)
     (expect (treemacs--format-bookmark-title (treemacs-current-button)) :to-equal expected-2)
     (forward-line 1)
     (expect (treemacs--format-bookmark-title (treemacs-current-button)) :to-equal expected-3))))

(describe "treemacs-collect-child-nodes"

  (it "Finds nothing for last node"
    (with-temp-buffer
      (insert (propertize "Root" 'button t :depth 1))
      (insert "\n")
      (goto-char 0)
      (let* ((parent-btn (point-marker))
             (result (treemacs-collect-child-nodes parent-btn)))
        (expect result :to-be nil))))

  (it "Finds nothing for node without direct children"
    (with-temp-buffer
      (let* ((root1 (progn
                      (insert (propertize "Root1" 'button t 'category t :depth 1))
                      (beginning-of-line)
                      (point-marker)))
             (input (progn
                      (end-of-line)
                      (insert "\n" (propertize " Input" 'button t 'category t :depth 2 :parent root1))
                      (beginning-of-line)
                      (point-marker)))
             (_root2 (progn
                       (end-of-line)
                       (insert "\n" (propertize "Root2" 'button t 'category t :depth 1))
                       (beginning-of-line)
                       (point-marker))) )
        (-let [result (treemacs-collect-child-nodes input)]
          (expect result :to-be nil)))))

  (it "Finds only direct children"
    (with-temp-buffer
      (let* ((root1 (progn
                      (insert (propertize "Root1" 'button t 'category t :depth 1))
                      (beginning-of-line)
                      (point-marker)))
             (input (progn
                      (end-of-line)
                      (insert "\n" (propertize " Input" 'button t 'category t :depth 2 :parent root1))
                      (beginning-of-line)
                      (point-marker)))
             (child1 (progn
                       (end-of-line)
                       (insert "\n" (propertize "  Child1" 'button t 'category t :depth 3 :parent input))
                       (beginning-of-line)
                       (point-marker)))
             (_grand-child (progn
                             (end-of-line)
                             (insert "\n" (propertize "   Grand Child" 'button t 'category t :depth 4 :parent child1))
                             (beginning-of-line)
                             (point-marker)))
             (_child2 (progn
                        (end-of-line)
                        (insert "\n" (propertize "  Child2" 'button t 'category t :depth 3 :parent input))
                        (beginning-of-line)
                        (point-marker))) )
        (-let [result (-map #'treemacs--get-label-of (treemacs-collect-child-nodes input))]
          (expect result :to-have-same-items-as '("  Child1" "  Child2")))))))

(describe "treemacs--format-bookmark-title"
  (it "Uses the configured pattern"
    (test-treemacs--format-pattern "No replacements" "No replacements" "No replacements" "No replacements"))

  (it "Formats the project name"
    (test-treemacs--format-pattern "${project}" "Project" "Project" "Project"))

  (it "Formats the label"
    (test-treemacs--format-pattern "${label}" "Project" "directory" "file.txt"))

  (it "Formats the parent label"
    (test-treemacs--format-pattern "${label:1}" "" "Project" "directory"))

  (it "Formats the grandparent label"
    (test-treemacs--format-pattern "${label:2}" "" "" "Project"))

  (it "Formats the label path"
    (test-treemacs--format-pattern "${label-path}" "Project" "Project/directory" "Project/directory/file.txt"))

  (it "Formats the limited label path"
    (test-treemacs--format-pattern "${label-path:2}" "Project" "Project/directory" "directory/file.txt"))

  (it "Does not hang with negatie label path limit"
    (test-treemacs--format-pattern "${label-path:-2}" "Project" "Project/directory" "Project/directory/file.txt"))

  (it "Formats the file path"
    (test-treemacs--format-pattern "${file-path}" "/project" "/project/directory" "/project/directory/file.txt"))

  (it "Formats the limited file path"
    (test-treemacs--format-pattern "${file-path:2}" "/project" "/project/directory" "directory/file.txt"))

  (it "Does not hang with negative file path"
    (test-treemacs--format-pattern "${file-path:-1}" "" "" "")))

(describe "treemacs-dom-node->remove-collapse-keys!"
  (it "Removes and deletes all collapse entries"
    (with-temp-buffer
      (let* ((dom-node (treemacs-dom-node->create!
                        :key "Main Key"
                        :collapse-keys '("Key 1" "Key 2" "Key 3")))
             (treemacs-dom (ht ("Main Key" dom-node)
                               ("Key 1" dom-node)
                               ("Key 2" dom-node)
                               ("Key 3" dom-node))))
        (treemacs-dom-node->remove-collapse-keys! dom-node '("Key 1" "Key 3"))
        (expect (treemacs-find-in-dom "Main Key") :to-be dom-node)
        (expect (treemacs-find-in-dom "Key 2") :to-be dom-node)
        (expect (treemacs-find-in-dom "Key 1") :to-be nil)
        (expect (treemacs-find-in-dom "Key 3") :to-be nil)
        (expect (treemacs-dom-node->collapse-keys dom-node) :to-equal '("Key 2"))))))

(describe "treemacs--maybe-clean-buffers-on-workspace-switch"
  :var* ((tmp-file (make-temp-file "TESTFILE"))
         (scratch (get-buffer-create "*scratch*"))
         (file-buffer)
         (non-file-buffer)
         (fake-buffer-list))

  (before-each
    (setf file-buffer (find-file-noselect tmp-file)
          non-file-buffer (get-buffer-create "TESTBUFFER")
          fake-buffer-list (list file-buffer non-file-buffer scratch)))

  (after-all
    (when file-buffer (kill-buffer file-buffer))
    (when non-file-buffer (kill-buffer non-file-buffer))
    (delete-file tmp-file))

  (it "Does nothing when cleanup is set to 'nil'"
    (-let [treemacs-workspace-switch-cleanup nil]
      (spy-on #'buffer-list :and-return-value fake-buffer-list)
      (treemacs--maybe-clean-buffers-on-workspace-switch treemacs-workspace-switch-cleanup)
      (expect (buffer-live-p scratch) :to-be t)
      (expect (buffer-live-p file-buffer) :to-be t)
      (expect (buffer-live-p non-file-buffer) :to-be t)))

  (it "Kills file buffers when cleanup is set to 'files'"
    (-let [treemacs-workspace-switch-cleanup 'files]
      (spy-on #'buffer-list :and-return-value fake-buffer-list)
      (treemacs--maybe-clean-buffers-on-workspace-switch treemacs-workspace-switch-cleanup)
      (expect (buffer-live-p scratch) :to-be t)
      (expect (buffer-live-p file-buffer) :to-be nil)
      (expect (buffer-live-p non-file-buffer) :to-be t)))

  (it "Kills all buffers when cleanup is set to 'all'"
    (-let [treemacs-workspace-switch-cleanup 'all]
      (spy-on #'buffer-list :and-return-value fake-buffer-list)
      (treemacs--maybe-clean-buffers-on-workspace-switch treemacs-workspace-switch-cleanup)
      (expect (buffer-live-p scratch) :to-be t)
      (expect (buffer-live-p file-buffer) :to-be nil)
      (expect (buffer-live-p non-file-buffer) :to-be nil))))

(describe "treemacs--find-repeated-file-name"
  (before-each
    (fset 'fake-file-exists
          (lambda (p)
            (pcase p
              ("/a/file.el" t)
              ("/c/file" t)
              ((guard (and (s-starts-with? "/b/" p)
                           (not (s-contains? "5" p))))
               t)
              ((guard (and (s-starts-with? "/d/" p)
                           (not (s-contains? "5" p))))
               t) )))
    (spy-on 'file-exists-p :and-call-fake 'fake-file-exists))

  (it "Returns input when it does not already exist"
    (expect (treemacs--find-repeated-file-name "/X/Y/Z")
            :to-equal "/X/Y/Z"))

  (it "Find a (Copy 1) file with extension"
    (expect (treemacs--find-repeated-file-name "/a/file.el")
            :to-equal "/a/file (Copy 1).el"))

  (it "Find a (Copy 5) file with extension"
    (expect (treemacs--find-repeated-file-name "/b/file.el")
            :to-equal "/b/file (Copy 5).el"))

  (it "Find a (Copy 1) file without extension"
    (expect (treemacs--find-repeated-file-name "/c/file")
            :to-equal "/c/file (Copy 1)"))

  (it "Find a (Copy 5) file without extension"
    (expect (treemacs--find-repeated-file-name "/d/file")
            :to-equal "/d/file (Copy 5)")))

(describe "treemacs--tokenize-path"

  (it "Throws when path is nil"
    (expect (treemacs--tokenize-path nil "/a")
            :to-throw))

  (it "Throws when exclude-prefix is longer than path"
    (expect (treemacs--tokenize-path "/a" "/a/b")
            :to-throw))

  (it "Returns nothing when path and exclude-prefix are equally long"
    (expect (treemacs--tokenize-path "/a/b" "/a/b")
            :to-be nil))

  (it "Tokenizes everything when exclude-prefix is nil"
    (expect (treemacs--tokenize-path "/a/b/c/d" nil)
            :to-equal '("a" "b" "c" "d")))

  (it "Tokenizes everything when exclude-prefix is empty"
    (expect (treemacs--tokenize-path "/a/b/c/d" "")
            :to-equal '("a" "b" "c" "d")))

  (it "Tokenizes everything past the exclude-prefix"
    (expect (treemacs--tokenize-path "/a/b/c/d" "/a/b")
            :to-equal '("c" "d"))))

(describe "Workspaces"

  (describe "treemacs-find-workspace"

    (describe "By Name"
      (it "Finds the right workspace"
        (let* ((ws1 (treemacs-workspace->create! :name "A"))
               (ws2 (treemacs-workspace->create! :name "B"))
               (ws3 (treemacs-workspace->create! :name "C"))
               (treemacs--workspaces (list ws1 ws2 ws3)))
          (expect (treemacs-find-workspace-by-name "B") :to-equal ws2)))

      (it "Returns nil when no workspaceis found"
        (let* ((ws1 (treemacs-workspace->create! :name "A"))
               (ws2 (treemacs-workspace->create! :name "B"))
               (ws3 (treemacs-workspace->create! :name "C"))
               (treemacs--workspaces (list ws1 ws2 ws3)))
          (expect (treemacs-find-workspace-by-name "X") :to-be nil))))

    (describe "By Path"

      (it "Finds the right workspace"
        (let* ((ws1 (treemacs-workspace->create! :projects (list (treemacs-project->create! :path "A"))))
               (ws2 (treemacs-workspace->create! :projects (list (treemacs-project->create! :path "B"))))
               (ws3 (treemacs-workspace->create! :projects (list (treemacs-project->create! :path "C"))))
               (treemacs--workspaces (list ws1 ws2 ws3)))
          (expect (treemacs-find-workspace-by-path "B") :to-equal ws2)))

      (it "Returns nil when no workspaceis found"
        (let* ((ws1 (treemacs-workspace->create! :projects (list (treemacs-project->create! :path "A"))))
               (ws2 (treemacs-workspace->create! :projects (list (treemacs-project->create! :path "B"))))
               (ws3 (treemacs-workspace->create! :projects (list (treemacs-project->create! :path "C"))))
               (treemacs--workspaces (list ws1 ws2 ws3)))
          (expect (treemacs-find-workspace-by-path "X") :to-be nil))))

    (describe "By Predicate"

      (it "Finds the right workspace"
        (let* ((ws1 (treemacs-workspace->create! :name "A"))
               (ws2 (treemacs-workspace->create! :name "B" :projects (list (treemacs-project->create! :path "B"))))
               (ws3 (treemacs-workspace->create! :name "B"))
               (treemacs--workspaces (list ws1 ws2 ws3)))
          (expect (treemacs-find-workspace-where (lambda (ws) (treemacs-workspace->projects ws)))
                  :to-equal ws2)))

      (it "Returns nil when no workspaceis found"
        (let* ((ws1 (treemacs-workspace->create! :name "A"))
               (ws2 (treemacs-workspace->create! :name "B"))
               (ws3 (treemacs-workspace->create! :name "C"))
               (treemacs--workspaces (list ws1 ws2 ws3)))
          (expect (treemacs-find-workspace-where (lambda (ws) (treemacs-workspace->projects ws)))
                  :to-be  nil)))))

  (describe "treemacs-create-workspace"

    (describe "Failures"

      (it "Fails when name is empty"
        (expect (treemacs-do-create-workspace "")
                :to-equal
                '(invalid-name "")))

      (it "Fails when name is blank"
        (expect (treemacs-do-create-workspace " ")
                :to-equal
                '(invalid-name " ")))

      (it "Fails when name contains newlines"
        (expect (treemacs-do-create-workspace "a\nb")
                :to-equal
                '(invalid-name "a\nb")))

      (it "Fails when name is a duplicate"
        (let* ((ws (treemacs-workspace->create! :name "A"))
               (treemacs--workspaces (list ws)))
          (expect (treemacs-do-create-workspace "A")
                  :to-equal
                  `(duplicate-name ,ws)))))

    (describe "Successes"
      :var* ((treemacs--workspaces nil))

      (before-each
        (setf treemacs--workspaces nil)
        (spy-on #'treemacs--persist nil))

      (it "Adds the workspace"
        (treemacs-do-create-workspace "Valid Name")
        (expect (treemacs-find-workspace-by-name "Valid Name") :to-be-truthy))
      (it "Persists the workspace"
        (treemacs-do-create-workspace "Valid Name")
        (expect #'treemacs--persist :to-have-been-called))
      (it "Returns the created workspace"
        (expect (car (treemacs-do-create-workspace "Valid Name"))
                :to-equal
                'success))))

  (describe "treemacs-do-remove-workspace"

    (describe "Failures"

      (it "Cannot delete the last workspace"
        (let* ((ws (treemacs-workspace->create!))
               (treemacs--workspaces (list ws)))
          (expect (treemacs-do-remove-workspace)
                  :to-equal
                  'only-one-workspace)))

      (it "Can be canceled by the user"
        (spy-on #'yes-or-no-p :and-return-value nil)

        (let* ((ws1 (treemacs-workspace->create! :name "A"))
               (ws2 (treemacs-workspace->create! :name "B"))
               (treemacs--workspaces (list ws1 ws2)))
          (expect (treemacs-do-remove-workspace "A" :ask-to-confirm)
                  :to-equal
                  'user-cancel)))

      (it "Only deleted workspaces that exist"
        (spy-on #'yes-or-no-p :and-return-value nil)

        (let* ((ws1 (treemacs-workspace->create! :name "A"))
               (ws2 (treemacs-workspace->create! :name "B"))
               (treemacs--workspaces (list ws1 ws2)))
          (expect (treemacs-do-remove-workspace "X")
                  :to-equal
                  '(workspace-not-found "X")))))

    (describe "Successes"

      (it "Deletes workspace that is given"
        (let* ((ws1 (treemacs-workspace->create! :name "A"))
               (ws2 (treemacs-workspace->create! :name "B"))
               (treemacs--workspaces (list ws1 ws2)))
          (expect (treemacs-do-remove-workspace "B")
                  :to-equal
                  `(success ,ws2 ,(list ws1)))))

      (it "Deletes workspace that selected interactively"
        (spy-on #'completing-read :and-return-value "B")
        (let* ((ws1 (treemacs-workspace->create! :name "A"))
               (ws2 (treemacs-workspace->create! :name "B"))
               (treemacs--workspaces (list ws1 ws2)))
          (expect (treemacs-do-remove-workspace)
                  :to-equal
                  `(success ,ws2 ,(list ws1)))))))

  (describe "treemacs-do-switch-to-workspace"

    (before-each
      (spy-on #'treemacs--restore :and-return-value nil)
      (spy-on #'treemacs--invalidate-buffer-project-cache :and-return-value nil)
      (spy-on #'treemacs--rerender-after-workspace-change :and-return-value nil))

    (describe "Failures"

      (it "Fails when there is only 1 workspace"
        (-let [treemacs--workspaces (list (treemacs-workspace->create! :name "A"))]
          (expect (treemacs-do-switch-workspace)
                  :to-equal 'only-one-workspace)))

      (it "Fails when workspace cannot be found"
        (-let [treemacs--workspaces
               (list (treemacs-workspace->create! :name "A")
                     (treemacs-workspace->create! :name "B"))]
          (expect (treemacs-do-switch-workspace "X")
                  :to-equal '(workspace-not-found "X")))))

    (describe "Successes"

      (it "Succeeds when workspace is given by name"
        (let* ((ws1 (treemacs-workspace->create! :name "A"))
               (ws2 (treemacs-workspace->create! :name "B"))
               (treemacs--workspaces (list ws1 ws2)))
          (expect (treemacs-do-switch-workspace "B")
                  :to-equal `(success ,ws2))))

      (it "Succeeds when workspace is given directly"
        (let* ((ws1 (treemacs-workspace->create! :name "A"))
               (ws2 (treemacs-workspace->create! :name "B"))
               (treemacs--workspaces (list ws1 ws2)))
          (expect (treemacs-do-switch-workspace ws2)
                  :to-equal `(success ,ws2)))))))

(describe "treemacs--select-file-from-current-btn"
  :var (btn)

  (before-each
    (fset
     'make-btn
     (lambda (&rest props)
       (insert "abc")
       (goto-char 0)
       (put-text-property 1 3 :path "/a/b/c")
       (put-text-property 1 3 :button t)
       (when props
         (apply #'put-text-property 1 3 props))
       (setf btn (point-marker))))
    (spy-on #'treemacs-current-button :and-return-value btn)
    (spy-on #'file-directory-p :and-return-value nil)
    (spy-on #'file-regular-p :and-return-value nil))

  (it "returns interactively chosen path for flattened nodes"
    (with-temp-buffer
      (with-no-warnings (make-btn :collapsed '(2 "/a" "/a/b" "/a/b/c")))
      (spy-on #'completing-read :and-return-value "/a/b")
      (expect (treemacs--select-file-from-btn btn "prompt")
              :to-equal "/a/b")))

  (it "returns path when it is a directory"
    (with-temp-buffer
      (with-no-warnings (make-btn))
      (spy-on #'file-directory-p :and-return-value t)
      (expect (treemacs--select-file-from-btn btn "prompt")
              :to-equal "/a/b/c")))

  (it "returns parent path when path is file and dir-only is set"
    (with-temp-buffer
      (with-no-warnings (make-btn))
      (spy-on #'file-regular-p :and-return-value t)
      (expect (treemacs--select-file-from-btn btn "prompt" :dir-only)
              :to-equal "/a/b")))

  (it "returns path when it is a file and dir-only is not set"
    (with-temp-buffer
      (with-no-warnings (make-btn))
      (spy-on #'file-regular-p :and-return-value t)
      (expect (treemacs--select-file-from-btn btn "prompt" nil)
              :to-equal "/a/b/c")))

  (it "returns HOME when path is not a string"
    (with-temp-buffer
      (with-no-warnings (make-btn :path '(a b c)))
      (spy-on #'file-regular-p :and-return-value t)
      (expect (treemacs--select-file-from-btn btn "prompt")
              :to-equal (expand-file-name "~")))) )

(describe "treemacs--select-workspace-by-name"
  (before-each
    (spy-on #'treemacs--maybe-load-workspaces :and-return-value nil))

  (it "returns match for only 1 workspace"
    (spy-on #'completing-read :and-return-value "WS")
    (let* ((ws (treemacs-workspace->create! :name "WS"))
           (treemacs--workspaces (list ws)))
      (expect (treemacs--select-workspace-by-name) :to-be ws)))

  (it "returns match for more than 1 workspace"
    (spy-on #'completing-read :and-return-value "WS2")
    (let* ((ws1 (treemacs-workspace->create! :name "WS1"))
           (ws2 (treemacs-workspace->create! :name "WS2"))
           (treemacs--workspaces (list ws1 ws2)))
      (expect (treemacs--select-workspace-by-name) :to-be ws2)))

  (it "forces a name selection"
    (spy-on #'completing-read :and-call-fake
            (-let [c 0]
              (lambda (&rest _)
                (if (> c 3)
                    "WS2"
                  (cl-incf c)
                  ""))))
    (let* ((ws1 (treemacs-workspace->create! :name "WS1"))
           (ws2 (treemacs-workspace->create! :name "WS2"))
           (treemacs--workspaces (list ws1 ws2)))

      (expect (treemacs--select-workspace-by-name) :to-be ws2))))

(describe "treemacs--find-workspace-by-name"
  (before-each
    (spy-on #'treemacs--maybe-load-workspaces :and-return-value nil))

  (it "returns match for only 1 workspace"
    (let* ((ws (treemacs-workspace->create! :name "WS1"))
           (treemacs--workspaces (list ws)))
      (expect (treemacs--find-workspace-by-name "WS1") :to-be ws)))

  (it "returns match for more than 1 workspace"
    (let* ((ws1 (treemacs-workspace->create! :name "WS1"))
           (ws2 (treemacs-workspace->create! :name "WS2"))
           (treemacs--workspaces (list ws1 ws2)))
      (expect (treemacs--find-workspace-by-name "WS2") :to-be ws2)))

  (it "returns nil when there is no match"
    (let* ((ws1 (treemacs-workspace->create! :name "WS1"))
           (ws2 (treemacs-workspace->create! :name "WS2"))
           (treemacs--workspaces (list ws1 ws2)))
      (expect (treemacs--find-workspace-by-name "X") :to-be nil))))

(describe "annotations"

  (describe "cleanup"

    (it "won't remove annotation if it has a face"
      (-let [treemacs--annotation-store (make-hash-table :size 200 :test 'equal)]
        (treemacs-set-annotation-face "Path" 'face "Source")
        (-let [ann (treemacs-get-annotation "Path")]
          (treemacs--remove-annotation-if-empty ann "Path")
          (expect (treemacs-get-annotation "Path") :to-be ann))))

    (it "won't remove annotation if it has a git face"
      (let* ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal))
             (ann (treemacs-annotation->create!)))
        (ht-set! treemacs--annotation-store "Path" ann)
        (setf (treemacs-annotation->git-face ann) 'face)
        (treemacs--remove-annotation-if-empty ann "Path")
        (expect (treemacs-get-annotation "Path") :to-be ann)))

    (it "won't remove annotation if it has a suffix"
      (-let [treemacs--annotation-store (make-hash-table :size 200 :test 'equal)]
        (treemacs-set-annotation-suffix "Path" "Suffix" "Source")
        (-let [ann (treemacs-get-annotation "Path")]
          (treemacs--remove-annotation-if-empty ann "Path")
          (expect (treemacs-get-annotation "Path") :to-be ann))))

    (it "removes empty annotation"
      (let* ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal))
             (ann (treemacs-annotation->create!)))
        (ht-set! treemacs--annotation-store "Path" ann)
        (treemacs--remove-annotation-if-empty ann "Path")
        (expect (treemacs-get-annotation "Path") :to-be nil))))

  (describe "faces"

    (describe "add"

      (it "saves single value"
        (-let [treemacs--annotation-store (make-hash-table :size 200 :test 'equal)]
          (treemacs-set-annotation-face "Path" 'face "Source")
          (-let [ann (treemacs-get-annotation "Path")]
            (expect (treemacs-annotation->face ann)
                    :to-equal '(("Source" . face)))
            (expect (treemacs-annotation->face-value ann)
                    :to-equal '(face)))))

      (it "saves multiple values"
        (-let [treemacs--annotation-store (make-hash-table :size 200 :test 'equal)]
          (treemacs-set-annotation-face "Path" 'face1 "Source1")
          (treemacs-set-annotation-face "Path" 'face2 "Source2")
          (-let [ann (treemacs-get-annotation "Path")]
            (expect (treemacs-annotation->face ann)
                    :to-equal '(("Source1" . face1) ("Source2" . face2)))
            (expect (treemacs-annotation->face-value ann)
                    :to-equal '(face1 face2)))))

      (it "updates face for same source"
        (-let [treemacs--annotation-store (make-hash-table :size 200 :test 'equal)]
          (treemacs-set-annotation-face "Path" 'face1 "Source1")
          (treemacs-set-annotation-face "Path" 'face2 "Source1")
          (-let [ann (treemacs-get-annotation "Path")]
            (expect (treemacs-annotation->face ann)
                    :to-equal '(("Source1" . face2)))
            (expect (treemacs-annotation->face-value ann)
                    :to-equal '(face2)))))

      (it "includes git face as last value element"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal))
              (ann (treemacs-annotation->create!)))
          (ht-set! treemacs--annotation-store "Path" ann)
          (setf (treemacs-annotation->git-face ann) 'git-face)
          (treemacs-set-annotation-face "Path" 'face1 "Source1")
          (treemacs-set-annotation-face "Path" 'face2 "Source2")
          (-let [ann (treemacs-get-annotation "Path")]
            (expect (treemacs-annotation->face ann)
                    :to-equal '(("Source1" . face1) ("Source2" . face2)))
            (expect (treemacs-annotation->face-value ann)
                    :to-equal '(face1 face2 . git-face))))))

    (describe "remove"

      (it "does nothing when there is no face"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal)))
          (treemacs-remove-annotation-face "Path" "Source")
          (expect (ht-size treemacs--annotation-store) :to-equal 0)))

      (it "removes the last face"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal))
              (ann (treemacs-annotation->create!)))
          (ht-set! treemacs--annotation-store "Path" ann)
          (treemacs-set-annotation-face "Path" 'face "Source")
          (treemacs-remove-annotation-face "Path" "Source")
          (expect (ht-get treemacs--annotation-store "Path") :to-be ann)
          (expect (treemacs-annotation->face ann) :to-be nil)
          (expect (treemacs-annotation->face-value ann) :to-be nil)))

      (it "sets face value to the git face after removing the last face"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal))
              (ann (treemacs-annotation->create!)))
          (ht-set! treemacs--annotation-store "Path" ann)
          (setf (treemacs-annotation->git-face ann) 'git-face)
          (treemacs-set-annotation-face "Path" 'face "Source")
          (treemacs-remove-annotation-face "Path" "Source")
          (expect (ht-get treemacs--annotation-store "Path") :to-be ann)
          (expect (treemacs-annotation->face-value ann) :to-equal 'git-face)))

      (it "leaves faces from other sources"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal))
              (ann (treemacs-annotation->create!)))
          (ht-set! treemacs--annotation-store "Path" ann)
          (setf (treemacs-annotation->git-face ann) 'git-face)
          (treemacs-set-annotation-face "Path" 'face1 "Source1")
          (treemacs-set-annotation-face "Path" 'face2 "Source2")
          (treemacs-remove-annotation-face "Path" "Source1")
          (expect (ht-get treemacs--annotation-store "Path") :to-be ann)
          (expect (treemacs-annotation->face ann) :to-equal '(("Source2" . face2)))
          (expect (treemacs-annotation->face-value ann) :to-equal '(face2 . git-face)))))

    (describe "clear"

      (it "does nothing when there are no annotations"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal)))
          (treemacs-clear-annotation-faces "Source")
          (expect (ht-size treemacs--annotation-store) :to-be 0)))

      (it "does not remove faces for a different source"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal)))
          (treemacs-set-annotation-face "Path" 'face "Source1")
          (treemacs-clear-annotation-faces "Source2")
          (-let [ann (treemacs-get-annotation "Path")]
            (expect ann :not :to-be nil)
            (expect (treemacs-annotation->face ann) :to-equal '(("Source1" . face))))))

      (it "removes empty annotations"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal)))
          (treemacs-set-annotation-face "Path" 'face "Source")
          (treemacs-clear-annotation-faces "Source")
          (expect (treemacs-get-annotation "Path") :to-be nil)))

      (it "removes all faces for the given source"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal)))
          (treemacs-set-annotation-face "Path1" 'face1 "Source1")
          (treemacs-set-annotation-face "Path1" 'face2 "Source2")
          (treemacs-set-annotation-face "Path2" 'face1 "Source1")
          (treemacs-set-annotation-face "Path2" 'face3 "Source3")
          (treemacs-clear-annotation-faces "Source1")
          (let ((ann1 (treemacs-get-annotation "Path1"))
                (ann2 (treemacs-get-annotation "Path2")))
            (expect ann1 :not :to-be nil)
            (expect ann2 :not :to-be nil)
            (expect (treemacs-annotation->face ann1) :to-equal '(("Source2" . face2)))
            (expect (treemacs-annotation->face ann2) :to-equal '(("Source3" . face3))))))))

  (describe "suffixes"

    (describe "add"

      (it "saves single value"
        (-let [treemacs--annotation-store (make-hash-table :size 200 :test 'equal)]
          (treemacs-set-annotation-suffix "Path" "Suffix" "Source")
          (-let [ann (treemacs-get-annotation "Path")]
            (expect (treemacs-annotation->suffix ann)
                    :to-equal '(("Source" . "Suffix")))
            (expect (treemacs-annotation->suffix-value ann)
                    :to-equal "Suffix"))))

      (it "saves multiple values"
        (-let [treemacs--annotation-store (make-hash-table :size 200 :test 'equal)]
          (treemacs-set-annotation-suffix "Path" "Suffix1" "Source1")
          (treemacs-set-annotation-suffix "Path" "Suffix2" "Source2")
          (-let [ann (treemacs-get-annotation "Path")]
            (expect (treemacs-annotation->suffix ann)
                    :to-equal '(("Source1" . "Suffix1") ("Source2" . "Suffix2")))
            (expect (substring-no-properties (treemacs-annotation->suffix-value ann))
                    :to-equal "Suffix1 Suffix2"))))

      (it "updates suffix for same source"
        (-let [treemacs--annotation-store (make-hash-table :size 200 :test 'equal)]
          (treemacs-set-annotation-suffix "Path" "Suffix1" "Source1")
          (treemacs-set-annotation-suffix "Path" "Suffix2" "Source1")
          (-let [ann (treemacs-get-annotation "Path")]
            (expect (treemacs-annotation->suffix ann)
                    :to-equal '(("Source1" . "Suffix2")))
            (expect (substring-no-properties (treemacs-annotation->suffix-value ann))
                    :to-equal "Suffix2")))))

    (describe "remove"

      (it "does nothing when there is no suffix"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal)))
          (treemacs-remove-annotation-suffix "Path" "Source")
          (expect (ht-size treemacs--annotation-store) :to-equal 0)))

      (it "leaves suffixes from other sources"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal))
              (ann (treemacs-annotation->create!)))
          (ht-set! treemacs--annotation-store "Path" ann)
          (treemacs-set-annotation-suffix "Path" "Suffix1" "Source1")
          (treemacs-set-annotation-suffix "Path" "Suffix2" "Source2")
          (treemacs-remove-annotation-suffix "Path" "Source1")
          (expect (ht-get treemacs--annotation-store "Path") :to-be ann)
          (expect (treemacs-annotation->suffix ann) :to-equal '(("Source2" . "Suffix2")))
          (expect (treemacs-annotation->suffix-value ann) :to-equal "Suffix2"))))

    (describe "clear"

      (it "does nothing when there are no annotations"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal)))
          (treemacs-clear-annotation-faces "Source")
          (expect (ht-size treemacs--annotation-store) :to-be 0)))

      (it "does not remove suffixes for a different source"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal)))
          (treemacs-set-annotation-suffix "Path" "Suffix" "Source1")
          (treemacs-clear-annotation-suffixes "Source2")
          (-let [ann (treemacs-get-annotation "Path")]
            (expect ann :not :to-be nil)
            (expect (treemacs-annotation->suffix ann) :to-equal '(("Source1" . "Suffix"))))))

      (it "removes empty annotations"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal)))
          (treemacs-set-annotation-suffix "Path" "Suffix" "Source")
          (treemacs-clear-annotation-suffixes "Source")
          (expect (treemacs-get-annotation "Path") :to-be nil)))

      (it "removes all suffixes for the given source"
        (let ((treemacs--annotation-store (make-hash-table :size 200 :test 'equal)))
          (treemacs-set-annotation-suffix "Path1" "Suffix1" "Source1")
          (treemacs-set-annotation-suffix "Path1" "Suffix2" "Source2")
          (treemacs-set-annotation-suffix "Path2" "Suffix1" "Source1")
          (treemacs-set-annotation-suffix "Path2" "Suffix3" "Source3")
          (treemacs-clear-annotation-suffixes "Source1")
          (let ((ann1 (treemacs-get-annotation "Path1"))
                (ann2 (treemacs-get-annotation "Path2")))
            (expect ann1 :not :to-be nil)
            (expect ann2 :not :to-be nil)
            (expect (treemacs-annotation->suffix ann1) :to-equal '(("Source2" . "Suffix2")))
            (expect (treemacs-annotation->suffix ann2) :to-equal '(("Source3" . "Suffix3")))))))))
(describe "treemacs--prefix-arg-to-recurse-depth"

  (it "translates numbers literally"
    (dotimes (n 10)
      (expect (treemacs--prefix-arg-to-recurse-depth n) :to-be n)))

  (it "translates nil to 0"
      (expect (treemacs--prefix-arg-to-recurse-depth nil) :to-be 0))

  (it "translates everything else to 999"
    (expect (treemacs--prefix-arg-to-recurse-depth '(4)) :to-be 999)
    (expect (treemacs--prefix-arg-to-recurse-depth 1.0) :to-be 999)
    (expect (treemacs--prefix-arg-to-recurse-depth "a") :to-be 999)
    (expect (treemacs--prefix-arg-to-recurse-depth (treemacs-project->create!)) :to-be 999)))

(provide 'test-treemacs)

;;; treemacs-test.el ends here
