;;; hyrolo-tests.el --- unit tests for hyrolo.el         -*- lexical-binding: t; -*-

;; Author:       Mats Lidell <matsl@gnu.org>
;;
;; Orig-Date:    19-Jun-21 at 22:42:00
;; Last-Mod:     17-May-26 at 12:15:26 by Bob Weiner
;;
;; SPDX-License-Identifier: GPL-3.0-or-later
;;
;; Copyright (C) 2021-2026  Free Software Foundation, Inc.
;; See the "HY-COPY" file for license information.
;;
;; This file is part of GNU Hyperbole.

;;; Commentary:

;; "../hyrolo.el"

;;; Code:

(require 'ert)
(require 'ert-x)
(require 'hyrolo)
(require 'hyrolo-demo)
(require 'hy-test-dependencies) ;; can install el-mock
(require 'hy-test-helpers "test/hy-test-helpers")
(require 'rx)
(require 'hib-kbd)
(require 'kotl-mode)

(declare-function hy-test-helpers:consume-input-events "hy-test-helpers")

(ert-deftest hyrolo-tests--add--error-cases ()
  "Verify `hyrolo-add' error cases."
  (defvar hyrolo-file)
  (let ((hyrolo-file (make-temp-file "hypb" nil ".otl")))
    (unwind-protect
        (let ((hyrolo-file-list (list hyrolo-file)))
          (should-error (hyrolo-add ""))
          (should-error (hyrolo-add 'symbol))
          (should-error (hyrolo-add "a" ""))
          (should-error (hyrolo-add "a" 'symbol))
          (should-error (hyrolo-add "a/b/c"))
          ;; File not writable
          (with-mock
            (mock (file-writable-p hyrolo-file) => nil)
            (should-error (hyrolo-add "a")))
          ;; File not readable
          (with-mock
            (mock (file-readable-p hyrolo-file) => nil)
            (should-error (hyrolo-add "a"))))
      (hy-delete-file-and-buffer hyrolo-file))))

(ert-deftest hyrolo-tests--add-item-to-new-file ()
  "Verify `hyrolo-add' creates and visits the file buffer if file does not exist."
  (let ((new-file (expand-file-name (make-temp-name "hypb") "/tmp")))
    (unwind-protect
        (progn
          (should-not (file-exists-p new-file))
          (hyrolo-add "a" new-file)
          (should (equal (current-buffer) (get-file-buffer new-file))))
      (hy-delete-file-and-buffer new-file))))

(ert-deftest hyrolo-tests--add-items-at-multiple-levels ()
  "`hyrolo-add' can add items at different levels."
  (let ((hyrolo-file (make-temp-file "hypb" nil ".otl")))
    (unwind-protect
        (let ((hyrolo-file-list (list hyrolo-file)))
          (find-file (car (hyrolo-get-file-list)))
          (insert "===\nHdr\n===\n")
          (goto-char (point-min))
          (should (looking-at "==="))
          (hyrolo-add "a")
          (hyrolo-add "a/b")
          (hyrolo-add "a/b/c")
          (beginning-of-line)
          (should (looking-at-p "\\*\\*\\*   c")))
      (hy-delete-file-and-buffer hyrolo-file))))

(ert-deftest hyrolo-tests--add-items-interactive ()
  "`hyrolo-add' can add items when called interactively."
  (let ((hyrolo-file (make-temp-file "hypb" nil ".otl")))
    (unwind-protect
        (let ((hyrolo-file-list (list hyrolo-file)))
          (find-file (car (hyrolo-get-file-list)))
          (insert "===\nHdr\n===\n")
          (goto-char (point-min))
          (should (looking-at "==="))
          (hy-test-helpers:ert-simulate-keys "item\n"
            (call-interactively #'hyrolo-add))
          (beginning-of-line)
          (should (looking-at-p "\\*   item\n"))
          ;; hyrolo-add in mail buffer
          (with-mock
            (mock (hyrolo-name-and-email) => (list "first, last" "email"))
            (hy-test-helpers:ert-simulate-keys "\n" ;; Confirm proposed name
              (call-interactively #'hyrolo-add))
            (beginning-of-line)
            (should (looking-at-p "\\*   first, last\t\t<email>\n"))))
      (hy-delete-file-and-buffer hyrolo-file))))

(ert-deftest hyrolo-tests--demo-search-work ()
  "Use demo example and search for work should match work."
  (unwind-protect
      (progn
        (load "../hyrolo-demo")
        (execute-kbd-macro (kbd "C-x 4r work RET"))
        (should (string= (buffer-name) hyrolo-display-buffer))
        (should (looking-at "======"))
        (forward-line 5)
        (should (looking-at "\\*.*Work")))
    (hyrolo-demo-quit)))

(ert-deftest hyrolo-tests--demo-tab-jump-to-first-match ()
  "{TAB} shall jump to first match."
  (unwind-protect
      (progn
        (load "../hyrolo-demo")
        (execute-kbd-macro (kbd "C-x 4r work RET TAB"))
        (should (string= (buffer-name) hyrolo-display-buffer))
        (should (looking-at "Work")))
    (hyrolo-demo-quit)))

(ert-deftest hyrolo-tests--demo-toggle-visibility ()
  "Keys {h} and {a} shall toggle visibility."
  (unwind-protect
      (progn
        (load "../hyrolo-demo")
        (execute-kbd-macro (kbd "C-x 4r work RET TAB"))
        (should (string= (buffer-name) hyrolo-display-buffer))
        (should (looking-at "Work"))

        (execute-kbd-macro (kbd "h"))
        (end-of-line)
        (should (get-char-property (point) 'invisible))

        (execute-kbd-macro (kbd "a"))
        (should-not (get-char-property (point) 'invisible))

        (execute-kbd-macro (kbd "h"))
        (end-of-line)
        (should (get-char-property (point) 'invisible))

        (execute-kbd-macro (kbd "s"))
        (should-not (get-char-property (point) 'invisible)))
    (hyrolo-demo-quit)))

(ert-deftest hyrolo-tests--demo-show-overview ()
  "Key {o} shall show overview."
  (unwind-protect
      (progn
        (load "../hyrolo-demo")
        (execute-kbd-macro (kbd "C-x 4r work RET TAB"))
        (should (string= (buffer-name) hyrolo-display-buffer))
        (should (looking-at "work"))

        (execute-kbd-macro (kbd "o"))
	(forward-line 1)
        (end-of-line)
        (should (get-char-property (point) 'invisible))

        ;; Check next match is an outline
        (execute-kbd-macro (kbd "TAB"))
        (end-of-line)
        (should-not (get-char-property (point) 'invisible)))
    (hyrolo-demo-quit)))

(ert-deftest hyrolo-tests--demo-move-to-beginning-and-end-of-file ()
  "*HyRolo* keys {<} and {>} move to begin and end of file, respectively."
  (unwind-protect
      (progn
        (load "../hyrolo-demo")
        (execute-kbd-macro (kbd "C-x 4r work RET TAB"))
        (should (string= (buffer-name) hyrolo-display-buffer))
        (should (looking-at "work"))

        (execute-kbd-macro (kbd "<"))
        (should (equal (point) (point-min)))

        (execute-kbd-macro (kbd ">"))
        (should (equal (point) (point-max))))
    (hyrolo-demo-quit)))

(ert-deftest hyrolo-tests--demo-move-to-beginning-and-end-of-entry ()
  "*HyRolo* keys {,} and {.} move to begin and end of an entry, respectively."
  (unwind-protect
      (progn
        (load "../hyrolo-demo")
        (execute-kbd-macro (kbd "C-x 4r work RET TAB"))
        (should (string= (buffer-name) hyrolo-display-buffer))

        (execute-kbd-macro (kbd "\C-u,n"))
	(should (looking-at "\\*\\*\\s-+Hansen"))

        (execute-kbd-macro (kbd "."))
	(should (looking-at "\\s-?\\*\\*\\*\\s-+Dunn")))
    (hyrolo-demo-quit)))

(ert-deftest hyrolo-tests--demo-move-between-entries-on-same-level ()
  "Key {n} shall move to the next cell, {f} the next same level cell,
and {b} the previous same level cell."
  (unwind-protect
      (progn
        (load "../hyrolo-demo")
        (execute-kbd-macro (kbd "C-x 4r com RET TAB"))
        (should (string= (buffer-name) hyrolo-display-buffer))
        (execute-kbd-macro (kbd "<"))
        (should (equal (point) (point-min)))

	(hyrolo-hdr-move-after-p)
        (should (looking-at "\\*\\*\\s-+Strong"))

        (execute-kbd-macro (kbd "f"))
        (should (looking-at "\\*\\*\\s-+Hansen"))

        (execute-kbd-macro (kbd "b"))
        (should (looking-at "\\*\\*\\s-+Strong")))
    (hyrolo-demo-quit)))

(ert-deftest hyrolo-tests--demo-no-following-same-level-heading ()
  "Error when trying to move to non existing next level heading."
  (unwind-protect
      (progn
        (load "../hyrolo-demo")
        (execute-kbd-macro (kbd "C-x 4r com RET TAB"))

        (should (string= (buffer-name) hyrolo-display-buffer))
        (execute-kbd-macro (kbd "<"))
        (should (equal (point) (point-min)))

	(hyrolo-hdr-move-after-p)
        (should (looking-at "\\*\\*\\s-+Strong"))

        (execute-kbd-macro (kbd "n"))
        (should (looking-at "\\*\\*\\*\\s-+Smith"))

        (condition-case err
            (execute-kbd-macro (kbd "f"))
          (error
           (progn
             (should (equal (car err) 'error))
             (should (string-match "No following same-level heading" (cadr err)))))))
    (hyrolo-demo-quit)))

(ert-deftest hyrolo-tests--sort ()
  "HyRolo files can be sorted."
  (let ((hyrolo-file (make-temp-file "hypb" nil ".otl")))
    (unwind-protect
        (let ((hyrolo-file-list (list hyrolo-file))
              (hyrolo-date-format "%m/%d/%Y"))
          (hyrolo-find-file (car (hyrolo-get-file-list)))
          (dolist (v '(:interactive nil))
            (erase-buffer)
            (insert "===\nHdr\n===\n")
            (goto-char (point-min))
            (should (looking-at "==="))
            (hyrolo-add "c")
            (hyrolo-add "b")
            (hyrolo-add "a")
            (hyrolo-add "b/d")

	    ;; Verify insertion order and following date on separate line
            (goto-char (point-min))
            (should (looking-at "==="))
            (dolist (insertion-order '("a" "b" "d" "c"))
              (goto-char (1+ (should (search-forward insertion-order))))
              (should (looking-at-p "^\t[0-9/]+$")))

            (if (eq v :interactive)
                (hy-test-helpers:ert-simulate-keys "\n" ;; Confirm proposed name
                  (call-interactively #'hyrolo-sort))
              (hyrolo-sort))

	    ;; Verify sorted order and following date on separate line
            (goto-char (point-min))
            (should (looking-at "==="))
            (dolist (sorted-order '("a" "b" "d" "c"))
              (goto-char (1+ (should (search-forward sorted-order))))
              (should (looking-at-p "^\t[0-9/]+$")))))
      (hy-delete-file-and-buffer hyrolo-file))))

(ert-deftest hyrolo-tests--sort-records-at-different-levels ()
  "HyRolo can sort records at different levels."
  (let* ((hyrolo-file (make-temp-file "hypb" nil ".otl"
                                      (concat "* 2\n\t2022-03-20\n"
                                              "** 2\n\t2022-03-20\n"
                                              "*** 2\n\t2022-03-20\n"
                                              "*** 1\n\t2022-03-20\n"
                                              "** 1\n\t2022-03-20\n"
                                              "*** 2\n\t2022-03-20\n"
                                              "*** 1\n\t2022-03-20\n"
                                              "* 1\n\t2022-03-20\n"
                                              "** 2\n\t2022-03-20\n"
                                              "*** 2\n\t2022-03-20\n"
                                              "*** 1\n\t2022-03-20\n"
                                              "** 1\n\t2022-03-20\n"
                                              "*** 2\n\t2022-03-20\n"
                                              "*** 1\n\t2022-03-20\n")))
	 (hyrolo-file-list (list hyrolo-file))
         (sorted-hyrolo-file (concat "* 1\n\t2022-03-20\n"
                                     "** 1\n\t2022-03-20\n"
                                     "*** 1\n\t2022-03-20\n"
                                     "*** 2\n\t2022-03-20\n"
                                     "** 2\n\t2022-03-20\n"
                                     "*** 1\n\t2022-03-20\n"
                                     "*** 2\n\t2022-03-20\n"
                                     "* 2\n\t2022-03-20\n"
                                     "** 1\n\t2022-03-20\n"
                                     "*** 1\n\t2022-03-20\n"
                                     "*** 2\n\t2022-03-20\n"
                                     "** 2\n\t2022-03-20\n"
                                     "*** 1\n\t2022-03-20\n"
                                     "*** 2\n\t2022-03-20\n")))
    (unwind-protect
	(progn (hyrolo-find-file hyrolo-file)
	       (hyrolo-sort hyrolo-file)
               (should (string= (buffer-string) sorted-hyrolo-file)))
      (hy-delete-file-and-buffer hyrolo-file))))

(ert-deftest hyrolo-tests--fgrep-find-all-types-of-files ()
  "Verify that all types of files are found in an fgrep search."
  (let* ((folder (make-temp-file "hypb" t))
         (prefix (expand-file-name "hypb" folder))
         (org-file (make-temp-file prefix nil ".org" "* string\n"))
         (kotl-file (make-temp-file prefix nil ".kotl" "1.  string"))
         (md-file (make-temp-file prefix nil ".md" "# string\n"))
         (outl-file (make-temp-file prefix nil ".otl" "* string\n"))
         (hyrolo-file-list (list folder)))
    (unwind-protect
        (progn
          (dolist (v '(:interactive nil))
            (if (eq v :interactive)
                (hy-test-helpers:ert-simulate-keys "string\n"
                  (should (= 4 (call-interactively #'hyrolo-fgrep))))
              (should (= 4 (hyrolo-fgrep "string"))))
            (should (string= (buffer-name) hyrolo-display-buffer))
            (should (= (how-many "@loc>") 4))
            (dolist (f (list org-file kotl-file md-file outl-file))
              (should (= (how-many (concat "@loc> \"" f "\"")) 1)))))
      (hy-delete-files-and-buffers (list org-file kotl-file md-file outl-file))
      (kill-buffer hyrolo-display-buffer)
      (delete-directory folder))))

(ert-deftest hyrolo-tests--fgrep-and-goto-next-visible-org-heading ()
  "Verify move to next heading, then action-key to go to record for org mode."
  (let* ((folder (make-temp-file "hypb" t))
         (prefix (expand-file-name "hypb" folder))
         (org-file (make-temp-file prefix nil ".org" "* heading\nstring\nmore\n"))
         (hyrolo-file-list (list folder)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-fgrep "string")))
          (should (string= (buffer-name) hyrolo-display-buffer))
          (should (= (how-many "@loc>") 1))
          (should (looking-at-p "==="))
          (hyrolo-outline-next-visible-heading 1)
          (should (looking-at-p "* heading"))
          (mocklet ((y-or-n-p => t))
            (action-key))
          (should (equal (current-buffer) (find-buffer-visiting org-file)))
          (should (looking-at-p "* heading")))
      (hy-delete-file-and-buffer org-file)
      (kill-buffer hyrolo-display-buffer)
      (delete-directory folder))))

(ert-deftest hyrolo-tests--fgrep-and-goto-next-visible-kotl-heading ()
  "Verify move to next heading, then action-key to go to record for kotl mode."
  (let* ((folder (make-temp-file "hypb" t))
         (prefix (expand-file-name "hypb" folder))
         (kotl-file (make-temp-file prefix nil ".kotl"))
         (hyrolo-file-list (list folder)))
    (unwind-protect
        (progn
          (find-file kotl-file)
          (insert "heading")
          (kotl-mode:newline 1)
          (insert "string")
          (kotl-mode:newline 1)
          (insert "more")
          (should (= 1 (hyrolo-fgrep "string")))
          (should (string= (buffer-name) hyrolo-display-buffer))
          (should (= (how-many "@loc>") 1))
          (should (looking-at-p "==="))
          (hyrolo-outline-next-visible-heading 1)
          (should (looking-at-p ".*1\\. heading"))
          (action-key)
          (should (equal (current-buffer) (find-buffer-visiting kotl-file)))
          (should (looking-at-p "heading")))
      (hy-delete-file-and-buffer kotl-file)
      (kill-buffer hyrolo-display-buffer)
      (delete-directory folder))))

(ert-deftest hyrolo-tests--fgrep-and-goto-next-visible-outl-heading ()
  "Verify move to next heading, then action-key to go to record for outline mode."
  (let* ((folder (make-temp-file "hypb" t))
         (prefix (expand-file-name "hypb" folder))
         (outl-file (make-temp-file prefix nil ".otl" "* heading\nstring\nmore\n"))
         (hyrolo-file-list (list folder)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-fgrep "string")))
          (should (string= (buffer-name) hyrolo-display-buffer))
          (should (= (how-many "@loc>") 1))
          (should (looking-at-p "==="))
          (hyrolo-outline-next-visible-heading 1)
          (should (looking-at-p "* heading"))
          (action-key)
          (should (equal (current-buffer) (find-buffer-visiting outl-file)))
          (should (looking-at-p "* heading")))
      (hy-delete-file-and-buffer outl-file)
      (kill-buffer hyrolo-display-buffer)
      (delete-directory folder))))

(ert-deftest hyrolo-tests--fgrep-and-goto-next-visible-md-heading ()
  "Verify move to next heading, then action-key to go to record for markdown mode."
  (let* ((folder (make-temp-file "hypb" t))
         (prefix (expand-file-name "hypb" folder))
         (md-file (make-temp-file prefix nil ".md" "# heading\nstring\nmore\n"))
         (hyrolo-file-list (list folder)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-fgrep "string")))
          (should (string= (buffer-name) hyrolo-display-buffer))
          (should (= (how-many "@loc>") 1))
          (should (looking-at-p "==="))
          (hyrolo-outline-next-visible-heading 1)
          (should (looking-at-p "# heading"))
          (action-key)
          (should (equal (current-buffer) (find-buffer-visiting md-file)))
          (should (looking-at-p "# heading")))
      (hy-delete-file-and-buffer md-file)
      (kill-buffer hyrolo-display-buffer)
      (delete-directory folder))))

(ert-deftest hyrolo-tests--fgrep-and-goto-next-visible-kotl-heading-level-2 ()
  "Verify move to next heading, then action-key to go to record for kotl mode.
Match a string in a level 2 child cell."
  (let* ((folder (make-temp-file "hypb" t))
         (prefix (expand-file-name "hypb" folder))
         (kotl-file (make-temp-file prefix nil ".kotl"))
         (hyrolo-file-list (list folder)))
    (unwind-protect
        (progn
          (find-file kotl-file)
          (insert "first")
          (kotl-mode:add-child)
          (insert "heading")
          (kotl-mode:newline 1)
          (insert "string")
          (kotl-mode:newline 1)
          (insert "more")
          (should (= 1 (hyrolo-fgrep "string")))
          (should (string= (buffer-name) hyrolo-display-buffer))
          (should (= (how-many "@loc>") 1))
          (should (looking-at-p "==="))
          (hyrolo-outline-next-visible-heading 1)
          (should (looking-at-p ".*1a\\. heading"))
          (action-key)
          (should (equal (current-buffer) (find-buffer-visiting kotl-file)))
          (should (looking-at-p "heading")))
      (hy-delete-file-and-buffer kotl-file)
      (kill-buffer hyrolo-display-buffer)
      (delete-directory folder))))

(ert-deftest hyrolo-tests--fgrep-and-goto-next-visible-kotl-heading-cell-2 ()
  "Verify move to next heading, then action-key to go to record for kotl mode.
Match a string in the second cell."
  (let* ((folder (make-temp-file "hypb" t))
         (prefix (expand-file-name "hypb" folder))
         (kotl-file (make-temp-file prefix nil ".kotl"))
         (hyrolo-file-list (list folder)))
    (unwind-protect
        (progn
          (find-file kotl-file)
          (insert "first")
          (kotl-mode:add-cell)
          (insert "heading")
          (kotl-mode:newline 1)
          (insert "string")
          (kotl-mode:newline 1)
          (insert "more")
          (should (= 1 (hyrolo-fgrep "string")))
          (should (string= (buffer-name) hyrolo-display-buffer))
          (should (= (how-many "@loc>") 1))
          (should (looking-at-p "==="))
          (hyrolo-outline-next-visible-heading 1)
          (should (looking-at-p ".*2\\. heading"))
          (action-key)
          (should (equal (current-buffer) (find-buffer-visiting kotl-file)))
          (should (looking-at-p "heading")))
      (hy-delete-file-and-buffer kotl-file)
      (kill-buffer hyrolo-display-buffer)
      (delete-directory folder))))

(ert-deftest hyrolo-tests--fgrep-move-test ()
  "Verify different move commands after `hyrolo-fgrep'."
  (let* ((kotl-file1 (hyrolo-tests--gen-kotl-outline "heading" "foo bar" 1))
         (kotl-file2 (hyrolo-tests--gen-kotl-outline "heading" "foo bar" 1))
         (hyrolo-file-list (list kotl-file1 kotl-file2))
         (h1_str  "   1\\. heading")
         (h1a_str  "     1a\\. heading 1"))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-fgrep "bar")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          (ert-info ("Hide first header move down using ?f")
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "h"))
            (execute-kbd-macro (kbd "f"))
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "f"))
            (should (looking-at-p h1_str))
            (let ((err (should-error (execute-kbd-macro (kbd "f")))))
              (should (string-match-p "No following same-level heading/header" (cadr err)))))

          (ert-info ("With hidden first header move up using ?b")
            (execute-kbd-macro (kbd "b"))
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "b"))
            (should (looking-at-p "==="))
            (should (bobp))
            (let ((err (should-error (execute-kbd-macro (kbd "b")))))
              (should (string-match-p "No previous same-level heading/header" (cadr err)))))

          (ert-info ("With hidden first header and move down using ?n")
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "n"))
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "n"))
            (should (looking-at-p h1_str))
            (execute-kbd-macro (kbd "n"))
            (should (looking-at-p h1a_str))
            (execute-kbd-macro (kbd "n"))
            (should (eobp)))

          (ert-info ("With hidden first header move up using ?p")
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p h1a_str))
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p h1_str))
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p "==="))
            (should (bobp)))

          (outline-show-all)

          (ert-info ("Move down using ?n")
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "n"))
            (should (looking-at-p h1_str))
            (execute-kbd-macro (kbd "n"))
            (should (looking-at-p h1a_str))
            (execute-kbd-macro (kbd "n"))
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "n"))
            (should (looking-at-p h1_str))
            (execute-kbd-macro (kbd "n"))
            (should (looking-at-p h1a_str))
            (execute-kbd-macro (kbd "n"))
            (should (eobp)))

          (ert-info ("Move up using ?p")
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p h1a_str))
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p h1_str))
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p h1a_str))
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p h1_str))
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p "==="))
            (should (bobp)))

          (ert-info ("Move down using ?f")
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "f"))
            (should (looking-at-p h1_str))
            (execute-kbd-macro (kbd "f"))
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "f"))
            (should (looking-at-p h1_str))
            (should-error (execute-kbd-macro (kbd "f"))))

          (ert-info ("Move up using ?b")
            (execute-kbd-macro (kbd "b"))
            (should (looking-at-p "==="))
            (execute-kbd-macro (kbd "b"))
            (should (looking-at-p h1_str))
            (execute-kbd-macro (kbd "b"))
            (should (looking-at-p "==="))
            (should-error (execute-kbd-macro (kbd "b")))
            (should (bobp))))
      ;; Unwind forms
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--get-file-list-change ()
  "Verify a change to hyrolo-file-list is noticed by hyrolo-get-file-list."
  (let* ((tmp-file (make-temp-file "hypb" nil ".org"))
         (hyrolo-file-list (list tmp-file)))
    (unwind-protect
        (let ((hl (hyrolo-get-file-list)))
          (should (= 1 (length hl)))
          (should (string= (car hl) tmp-file)))
      (hy-delete-file-and-buffer tmp-file))))

(ert-deftest hyrolo-tests--get-file-list-wrong-suffix ()
  "Verify files need to have the proper suffix in hyrolo-file-list."
  (let ((tmp-file (make-temp-file "hypb" nil)))
    (unwind-protect
        (should-error
         (let* ((hyrolo-boolean-only-flag t)
		(hyrolo-file-list (list tmp-file)))
           ()))
      (hy-delete-file-and-buffer tmp-file))))

(ert-deftest hyrolo-tests--get-file-list ()
  "Verify `hyrolo-get-file-list` includes added files."
  (let* ((folder (make-temp-file "hypb" t))
         (prefix (expand-file-name "hypb" folder))
         (org-file (make-temp-file prefix nil ".org"))
         (hyrolo-file-list (list folder)))
    (unwind-protect
        (progn
          (should (= 1 (length (hyrolo-get-file-list))))
          (let ((org2-file (make-temp-file prefix nil ".org")))
            (unwind-protect
		(progn (hyrolo-refresh-file-list)
                       (should (= 2 (length (hyrolo-get-file-list)))))
              (hy-delete-file-and-buffer org2-file))))
      (hy-delete-file-and-buffer org-file)
      (delete-directory folder))))

;; Outline movement tests
(defun hyrolo-tests--level-number (section depth)
  "Generate the number for the SECTION at DEPTH.

The format is the section followed by the depth given by the
sequence up to depth starting from 2.
  Depth 1:        section
  Depth <depth>:  section.2.3.4..<depth>"
  (let (result)
    (dotimes (d depth)
      (setq result
            (if (= 0 d)
                (number-to-string section)
              (concat result
                      "."
                      (number-to-string (+ 1 d))))))
    result))

(defun hyrolo-tests--generate-heading-contents-for-tests (heading-prefix-char heading section body depth)
  "Generate the HEADING and BODY contents for the SECTION with DEPTH."
  (let (result)
    (dotimes (d depth)
      (setq result
            (concat result
                    (make-string (1+ d) heading-prefix-char)
		    " " heading " " (hyrolo-tests--level-number section (1+ d)) "\n"
                    body " " (hyrolo-tests--level-number section (1+ d)) "\n")))
    result))

(defun hyrolo-tests--gen-outline (heading-prefix-char heading sections body depth)
  "Generate an outline structure suitable for hyrolo outline test.

The contents is constructed with an outline HEADING-PREFIX-CHAR,
HEADING and BODY text.  Each is repeated in SECTIONS with one set
of hierarchical headings to the specified DEPTH.

Example:
   * heading 1
   body 1
   ** heading 2
   body 1.2
   [...]
   * heading <sections>
   body <sections>
   ** heading <sections>.2
   body <section>.2
   [...]"
  (let (result)
    (dotimes (section sections)
      (setq result
            (concat result
                    (hyrolo-tests--generate-heading-contents-for-tests
		     heading-prefix-char heading (1+ section) body depth))))
    result))

(ert-deftest hyrolo-tests--hyrolo-grep-interactive ()
  "Verify `hyrolo-grep' works when called interactively.
This just verifies it looks good when called interactively.  Other tests
below verifies all the details."
  (let* ((org-file (make-temp-file "hypb" nil ".org"
                                   (hyrolo-tests--gen-outline ?* "heading" 2 "body" 2)))
         (hyrolo-file-list (list org-file)))
    (unwind-protect
        (progn
          (hy-test-helpers:ert-simulate-keys "body\n"
            (should (= 2 (call-interactively #'hyrolo-grep))))
          (should (string= hyrolo-display-buffer (buffer-name)))

          (should (looking-at-p "==="))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^\\* heading 1")))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--outline-next-visible-heading ()
  "Verify movement to next visible heading."
  (let* ((org-file (make-temp-file "hypb" nil ".org"
                                   (hyrolo-tests--gen-outline ?* "heading" 2 "body" 2)))
         (hyrolo-file-list (list org-file)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Move down
          (should (looking-at-p "==="))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^\\* heading 1"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^\\*\\* heading 1\\.2"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^\\* heading 2"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^\\*\\* heading 2\\.2"))
          (execute-kbd-macro (kbd "n"))
          (should (eobp))

          ;; Move back up
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^\\*\\* heading 2\\.2"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^\\* heading 2"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^\\*\\* heading 1\\.2"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^\\* heading 1"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "==="))
          (should (bobp)))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--outline-next-visible-heading-md ()
  "Verify movement to next visible heading."
  (let* ((md-file (make-temp-file "hypb" nil ".md"
                                  (hyrolo-tests--gen-outline ?# "heading" 2 "body" 2)))
         (hyrolo-file-list (list md-file)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Move down
          (should (looking-at-p "==="))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^# heading 1"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^## heading 1\\.2"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^# heading 2"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^## heading 2\\.2"))
          (execute-kbd-macro (kbd "n"))
          (should (eobp))

          ;; Move back up
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^## heading 2\\.2"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^# heading 2"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^## heading 1\\.2"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^# heading 1"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "==="))
          (should (bobp)))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--outline-next-visible-heading-all ()
  "Verify movement to next visible heading."
  (let* ((md-file (make-temp-file "hypb" nil ".md"
                                  (hyrolo-tests--gen-outline ?# "heading" 2 "body" 2)))
         (hyrolo-file-list (list md-file)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Move down
          (should (looking-at-p "==="))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^# heading 1"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^## heading 1\\.2"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^# heading 2"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^## heading 2\\.2"))
          (execute-kbd-macro (kbd "n"))
          (should (eobp))

          ;; Move back up
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^## heading 2\\.2"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^# heading 2"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^## heading 1\\.2"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^# heading 1"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "==="))
          (should (bobp)))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--outline-up-heading ()
  "Verify movement from sub heading to next heading one level above."
  (let* ((org-file (make-temp-file "hypb" nil ".org"
                                   (hyrolo-tests--gen-outline ?* "heading" 2 "body" 3)))
         (hyrolo-file-list (list org-file)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Move to last heading
          (goto-char (point-max))
          (forward-line -2)
          (should (looking-at-p "^\\*\\*\\* heading 2\\.2\\.3$"))
          (execute-kbd-macro (kbd "u"))
          (should (looking-at-p "^\\*\\* heading 2\\.2$"))
          (execute-kbd-macro (kbd "u"))
          (should (looking-at-p "^\\* heading 2$"))
          (should-error (execute-kbd-macro (kbd "u"))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-file-and-buffer org-file))))

(ert-deftest hyrolo-tests--outline-next-visible-heading-two-sections ()
  "Verify movement to next visible heading with two sections."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org"
                                    (hyrolo-tests--gen-outline ?* "heading-a" 1 "body-a" 2)))
         (md-file1 (make-temp-file "hypb" nil ".md"
                                   (hyrolo-tests--gen-outline ?# "heading-b" 1 "body-b" 2)))
         (hyrolo-file-list (list org-file1 md-file1)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Move down
          (should (looking-at-p "==="))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^\\* heading-a 1$"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^\\*\\* heading-a 1\\.2$"))
          (execute-kbd-macro (kbd "n"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^# heading-b 1$"))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^## heading-b 1\\.2$"))
          (execute-kbd-macro (kbd "n"))
          (should (eobp))

          ;; Move back up
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^## heading-b 1\\.2$"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^# heading-b 1$"))
          (execute-kbd-macro (kbd "p"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^\\*\\* heading-a 1\\.2$"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^\\* heading-a 1$"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "==="))
          (should (= 1 (line-number-at-pos))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(defun hyrolo-tests--gen-kotl-outline (heading body &optional depth)
  "Generate a temp file with kotl outline structure for hyrolo outline test.
Make cell start with HEADING and follow by next line BODY.  With
optional DEPTH the number of sub cells are created to that depth."
  (let ((kotl-file (make-temp-file "hypb" nil ".kotl")))
    (find-file kotl-file)
    (insert heading)
    (kotl-mode:newline 1)
    (insert body)
    (when (and depth (< 0 depth))
      (dotimes (d depth)
        (kotl-mode:add-child)
        (insert (format "%s %d" heading (1+ d)))
        (kotl-mode:newline 1)
        (insert (format "%s %d" body (1+ d)))))
    (hypb:save-buffer-silently)
    kotl-file))

(ert-deftest hyrolo-tests--outline-next-visible-heading-kotl ()
  "Verify movement to next visible heading with a kotl file."
  (let* ((kotl-file1 (hyrolo-tests--gen-kotl-outline "heading-kotl" "body-kotl"))
         (hyrolo-file-list (list kotl-file1)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          (should (looking-at-p "==="))
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^ +1\\. heading-kotl$"))
          (execute-kbd-macro (kbd "n"))
          (should (eobp))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "^ +1\\. heading-kotl$"))
          (execute-kbd-macro (kbd "p"))
          (should (looking-at-p "===")))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--outline-next-visible-heading-all-file-types ()
  "Verify movement to next visible heading with all files types present."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org"
                                    (hyrolo-tests--gen-outline ?* "heading-org" 1 "body-org" 1)))
         (otl-file1 (make-temp-file "hypb" nil ".otl"
                                    (hyrolo-tests--gen-outline ?* "heading-otl" 1 "body-otl" 1)))
         (md-file1 (make-temp-file "hypb" nil ".md"
                                   (hyrolo-tests--gen-outline ?# "heading-md" 1 "body-md" 1)))
         (kotl-file1 (hyrolo-tests--gen-kotl-outline "heading-kotl" "body-kotl"))
         (hyrolo-file-list (list org-file1 otl-file1 md-file1 kotl-file1)))
    (unwind-protect
        (progn
          (should (= 4 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Move down
          (dolist (v '("===" "^\\* heading-org 1$" "===" "^\\* heading-otl 1$"
                       "===" "^# heading-md 1$" "===" "^ +1\\. heading-kotl$"))
            (should (looking-at-p v))
            (execute-kbd-macro (kbd "n")))
          (should (eobp))

          ;; Move up
          (dolist (v '("^ +1\\. heading-kotl$" "===" "^# heading-md 1$" "==="
                       "^\\* heading-otl 1$" "===" "^\\* heading-org 1$" "==="))
            (execute-kbd-macro (kbd "p"))
            (should (looking-at-p v)))
          (should (= 1 (line-number-at-pos))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(defun hyrolo-tests--verify-hidden-line ()
  "Verify that a line is hidden."
  (save-excursion
    (end-of-line)
    (should (get-char-property (point) 'invisible))))

(defun hyrolo-tests--verify-not-hidden-line ()
  "Verify that a line is not hidden."
  (save-excursion
    (end-of-line)
    (should-not (get-char-property (point) 'invisible))))

(ert-deftest hyrolo-tests--outline-hide-show-heading ()
  "Verify hiding and showing headings."
  (let* ((org-file (make-temp-file "hypb" nil ".org"
                                   (hyrolo-tests--gen-outline ?* "heading" 1 "body" 2)))
         (hyrolo-file-list (list org-file)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Hide/Show first line hides whole section
          (should (looking-at-p "==="))
          (execute-kbd-macro (kbd "h"))
	  (hyrolo-tests--verify-hidden-line)
          (execute-kbd-macro (kbd "s"))
	  (hyrolo-tests--verify-not-hidden-line)

	  ;; Hide/Show first section heading
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^\\* heading 1$"))
          (execute-kbd-macro (kbd "h"))
	  (hyrolo-tests--verify-hidden-line)
	  (save-excursion
	    (forward-visible-line 1)
	    (should (eobp)))
          (execute-kbd-macro (kbd "s"))
	  (hyrolo-tests--verify-not-hidden-line)

	  ;; Hide/Show level 2 heading
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^\\*\\* heading 1\\.2$"))
          (execute-kbd-macro (kbd "h"))
	  (hyrolo-tests--verify-hidden-line)
	  (save-excursion
	    (forward-visible-line 1)
	    (should (eobp)))
          (execute-kbd-macro (kbd "s"))
	  (hyrolo-tests--verify-not-hidden-line))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--outline-show-when-moving-out-of-hidden-line ()
  "Verify region is shown after moving out of hidden area."
  (let* ((org-file (make-temp-file "hypb" nil ".org"
                                   (hyrolo-tests--gen-outline ?* "heading" 1 "body" 2)))
         (hyrolo-file-list (list org-file)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Hide first line hides whole section
          (should (looking-at-p "==="))
          (execute-kbd-macro (kbd "h"))
	  (hyrolo-tests--verify-hidden-line)

	  ;; Now expose just top-level headings and move to buffer beginning
          (execute-kbd-macro (kbd "t"))
          (execute-kbd-macro (kbd "<"))

	  ;; Move to first heading and back to top
          (execute-kbd-macro (kbd "n"))
          (should (looking-at-p "^\\* heading 1$"))
	  (should-not (get-char-property (point) 'invisible))
          (execute-kbd-macro (kbd "p"))
	  (should (and (looking-at-p "===") (= 1 (line-number-at-pos))))
	  (hyrolo-tests--verify-not-hidden-line))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--tab-through-matches ()
  "Verify tabbing through search matches.
File name contains the search string to verify it is not selected while
tabbing though the matches."
  (let* ((org-file (make-temp-file "hypb_body_" nil ".org"
                                   (hyrolo-tests--gen-outline ?* "heading" 2 "body" 2)))
         (hyrolo-file-list (list org-file)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Search Down
          (should (looking-at-p "==="))
          (execute-kbd-macro (kbd "TAB"))
          (should (looking-at-p "^body 1$"))
          (execute-kbd-macro (kbd "TAB"))
          (should (looking-at-p "^body 1\\.2"))
          (execute-kbd-macro (kbd "TAB"))
          (should (looking-at-p "^body 2$"))
          (execute-kbd-macro (kbd "TAB"))
          (should (looking-at-p "^body 2\\.2"))
          (should-error (execute-kbd-macro (kbd "TAB")))
          (should (looking-at-p "^body 2\\.2"))

          ;; Search Up
          (execute-kbd-macro (kbd "<backtab>"))
          (should (looking-at-p "^body 2$"))
          (execute-kbd-macro (kbd "<backtab>"))
          (should (looking-at-p "^body 1\\.2"))
          (execute-kbd-macro (kbd "<backtab>"))
          (should (looking-at-p "^body 1$"))
          (should-error (execute-kbd-macro (kbd "<backtab>"))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--edit-entry ()
  "Verify {e} brings up entry in new window."
  (let* ((org-file (make-temp-file "hypb" nil ".org"
                                   (hyrolo-tests--gen-outline ?* "heading" 1 "body" 2)))
         (hyrolo-file-list (list org-file)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Search Down
          (should (looking-at-p "==="))
          (execute-kbd-macro (kbd "TAB"))
          (should (looking-at-p "^body 1$"))

          ;; Edit record
          (mocklet ((y-or-n-p => t))
            (execute-kbd-macro (kbd "e")))
          (should (string= (buffer-name) (file-name-nondirectory org-file)))
          (should (looking-at-p "^body 1$"))

          ;; Edit next record
          (switch-to-buffer hyrolo-display-buffer)
          (execute-kbd-macro (kbd "TAB"))
          (should (looking-at-p "^body 1\\.2$"))
          (execute-kbd-macro (kbd "e"))
          (should (string= (buffer-name) (file-name-nondirectory org-file)))
          (should (looking-at-p "^body 1\\.2$"))
          )
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--forward-same-level-all-file-types-level1 ()
  "Verify forward and backward to first level headers and section lines.
All files types are present."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org"
                                    (hyrolo-tests--gen-outline ?* "heading-org" 2 "body-org" 1)))
         (md-file1 (make-temp-file "hypb" nil ".md"
                                   (hyrolo-tests--gen-outline ?# "heading-md" 2 "body-md" 1)))
         (otl-file1 (make-temp-file "hypb" nil ".otl"
                                    (hyrolo-tests--gen-outline ?* "heading-otl" 2 "body-otl" 1)))
         (kotl-file1 (hyrolo-tests--gen-kotl-outline "heading-kotl" "body-kotl"))
         (hyrolo-file-list (list org-file1 md-file1 otl-file1 kotl-file1)))
    (unwind-protect
        (progn
          (should (= 7 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Move forward
          (dolist (v '("^\\* heading-org 1$" "^\\* heading-org 2$"
		       "===" "^# heading-md 1$" "^# heading-md 2$"
                       "===" "^\\* heading-otl 1$" "^\\* heading-otl 2$"
		       "===" "^ +1\\. heading-kotl$"))
            (execute-kbd-macro (kbd "f"))
            (should (looking-at-p v)))

          ;; Move backward
          (dolist (v '("===" "^\\* heading-otl 2$" "^\\* heading-otl 1$"
		       "===" "^# heading-md 2$" "^# heading-md 1$"
                       "===" "^\\* heading-org 2$" "^\\* heading-org 1$"
		       "==="))
            (execute-kbd-macro (kbd "b"))
            (should (looking-at-p v)))
          (should (= 1 (line-number-at-pos))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--forward-same-level-all-file-types-level1-depth2 ()
  "Verify forward and backward to first level headers and section lines.
All files types are present with a max depth of 2 of the outline
structure."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org"
                                    (hyrolo-tests--gen-outline ?* "heading-org" 1 "body-org" 2)))
         (md-file1 (make-temp-file "hypb" nil ".md"
                                   (hyrolo-tests--gen-outline ?# "heading-md" 1 "body-md" 2)))
         (otl-file1 (make-temp-file "hypb" nil ".otl"
                                    (hyrolo-tests--gen-outline ?* "heading-otl" 1 "body-otl" 2)))
         (kotl-file1 (hyrolo-tests--gen-kotl-outline "heading-kotl" "body-kotl" 2))
         (hyrolo-file-list (list org-file1 md-file1 otl-file1 kotl-file1)))
    (unwind-protect
        (progn
          (should (= 4 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Move forward
          (dolist (v '("===" "^\\* heading-org 1$" "===" "^# heading-md 1$"
                       "===" "^\\* heading-otl 1$" "==="))
            (should (looking-at-p v))
            (execute-kbd-macro (kbd "f")))
          (should (looking-at-p "^ +1\\. heading-kotl$")) ; When on last match do not move further

          ;; Move backward
          (dolist (v '("===" "^\\* heading-otl 1$" "===" "^# heading-md 1$"
                       "===" "^\\* heading-org 1$" "==="))
            (execute-kbd-macro (kbd "b"))
            (should (looking-at-p v)))
          (should (= 1 (line-number-at-pos))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(defconst hyrolo-tests--outline-content-org
  "\
* h-org 1
body
** h-org 1.1
body
** h-org 1.2
body
*** h-org 1.2.1
body
* h-org 2
body
** h-org 2.1
body
"
  "Outline content for org files.")

(defun hyrolo-tests--modify-test-data (star type str)
  "Replace * with STAR and org with TYPE in STR.
Useful for creating outline and markdown test data from org examples."
  (replace-regexp-in-string
   "org" type
   (replace-regexp-in-string (regexp-quote "*") star str)))

(defconst hyrolo-tests--outline-content-otl
  (hyrolo-tests--modify-test-data "*" "otl" hyrolo-tests--outline-content-org)
  "Outline content for otl files.")

(defconst hyrolo-tests--outline-content-md
  (hyrolo-tests--modify-test-data "#" "md" hyrolo-tests--outline-content-org)
  "Outline content for markdown files.")

(ert-deftest hyrolo-tests--forward-same-level-org-level2 ()
  "Verify forward and backward to second level headers with org files."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (org-file2 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (hyrolo-file-list (list org-file1 org-file2)))
    (unwind-protect
        (progn
          (should (= 4 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Move to first second level header
          (search-forward "** h-org 1.1")
          (beginning-of-line)
          (should (looking-at-p "^\\*\\* h-org 1\\.1"))

          ;; Move forward same level
          (execute-kbd-macro (kbd "f"))
          (should (looking-at-p "^\\*\\* h-org 1\\.2"))

          ;; Multiple times does not move point when there are no more headers at the same level
          (should-error (execute-kbd-macro (kbd "f")))
	  (should (looking-at-p "^\\*\\* h-org 1\\.2"))

          ;; Move back on same level
          (execute-kbd-macro (kbd "b"))
          (should (looking-at-p "\\*\\* h-org 1\\.1"))

          ;; Moving up from first header on a level errors, also when repeated.
          (should-error (execute-kbd-macro (kbd "b")))
          (should (looking-at-p "^\\*\\* h-org 1\\.1"))
          (should-error (execute-kbd-macro (kbd "b")))
          (should (looking-at-p "^\\*\\* h-org 1\\.1")))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--forward-same-level-all-file-types-level2 ()
  "Verify forward and backward to second level headers with org files."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (otl-file1 (make-temp-file "hypb" nil ".otl" hyrolo-tests--outline-content-otl))
         (md-file1 (make-temp-file "hypb" nil ".md" hyrolo-tests--outline-content-md))
         (kotl-file1 (hyrolo-tests--gen-kotl-outline "heading-kotl" "body-kotl" 2))
         (hyrolo-file-list (list org-file1 otl-file1 md-file1 kotl-file1)))
    (unwind-protect
        (progn
          (should (= 7 (hyrolo-grep "body")))
          (should (string= hyrolo-display-buffer (buffer-name)))

          ;; Move to first second level header
          (search-forward "** h-org 1.1")
          (beginning-of-line)
          (should (looking-at-p "^\\*\\* h-org 1\\.1"))

          ;; Move forward same level
          (execute-kbd-macro (kbd "f"))
          (should (looking-at-p "^\\*\\* h-org 1\\.2"))

          ;; Multiple times does not move point when there are no more headers at the same level
          (should-error (execute-kbd-macro (kbd "f")))
	  (should (looking-at-p "^\\*\\* h-org 1\\.2"))

          ;; Move back on same level
          (execute-kbd-macro (kbd "b"))
          (should (looking-at-p "\\*\\* h-org 1\\.1"))

          ;; Moving up from first header on a level errors, also when repeated.
          (should-error (execute-kbd-macro (kbd "b")))
          (should (looking-at-p "^\\*\\* h-org 1\\.1"))
          (should-error (execute-kbd-macro (kbd "b")))
          (should (looking-at-p "^\\*\\* h-org 1\\.1")))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(defun hyrolo-tests--outline-as-string (&optional begin end)
  "Return buffer content as a string with hidden text replaced by ellipses.
The string contains what the outline actually looks like.  This
enables `string-match' tests for verifying text is hidden.  With
optional BEGIN and END only return that part of the buffer."
  (if (not begin) (setq begin (point-min)))
  (if (not end) (setq end (point-max)))
  (save-excursion
    (let ((result "")
          in-invisible-section)
      (goto-char begin)
      (while (< (point) end)
        (setq result
              (concat result
                      (if (get-char-property (point) 'invisible)
                          (cond ((not in-invisible-section)
                                 (setq in-invisible-section t)
                                 "...")
                                (t nil))
                        (progn
                          (if in-invisible-section
                              (setq in-invisible-section nil))
                          (char-to-string (char-after))))))
        (goto-char (1+ (point))))
      result)))

(ert-deftest hyrolo-tests--outline-hide-other ()
  "Verify `hyrolo-outline-hide-other' hides except current body, parent and top-level headings."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (hyrolo-file-list (list org-file1)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))

          ;; First line
          (should (= (point) 1))
          (hyrolo-outline-hide-other)
          (should (string-match-p
                   (concat "^\\(===+.*[^*]\\|" (regexp-quote "...\n") "\\)"
                           (regexp-quote "\
* h-org 1...
* h-org 2...
"
                                         ) "$")
                   (hyrolo-tests--outline-as-string)))

          ;; On first header
          (goto-char (point-min))
          (hyrolo-outline-show-all)
          (search-forward "* h-org 1")
          (beginning-of-line)
          (hyrolo-outline-hide-other)
          (should (string= "\
* h-org 1
body...
* h-org 2...
"
                           (hyrolo-tests--outline-as-string (point))))

          ;; On second header
          (goto-char (point-min))
          (hyrolo-outline-show-all)
          (search-forward "** h-org 1.1")
          (beginning-of-line)
          (hyrolo-outline-hide-other)
          (should (string= "\
** h-org 1.1
body...
* h-org 2...
"
                           (hyrolo-tests--outline-as-string (point)))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))


(ert-deftest hyrolo-tests--outline-hide-sublevels ()
  "Verify `hyrolo-outline-hide-sublevels' hides everything but the top levels."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (hyrolo-file-list (list org-file1)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))

          ;; First line
          (should (= (point) 1))
          (hyrolo-outline-hide-sublevels 1)
          (should (= (point) 1))
          (should (string-match-p
                   (concat "^\\(===+.*[^*]\\|" (regexp-quote "...\n") "\\)"
                           (regexp-quote "\
* h-org 1...
* h-org 2...
"
                                         ) "$")
                   (hyrolo-tests--outline-as-string)))

          ;; On first header
          (goto-char (point-min))
          (hyrolo-outline-show-all)
          (search-forward "* h-org 1")
          (beginning-of-line)
          (hyrolo-outline-hide-sublevels 1)
          (should (string= "\
* h-org 1...
* h-org 2...
"
                           (hyrolo-tests--outline-as-string (point))))

          ;; On second header
          (goto-char (point-min))
          (hyrolo-outline-show-all)
          (search-forward "** h-org 1.1")
          (beginning-of-line)
          (hyrolo-outline-hide-sublevels 1)
          (should (string= "\
1...
* h-org 2...
"
                           (hyrolo-tests--outline-as-string (point))))

          ;; First line - 2 levels
          (goto-char (point-min))
          (should (= (point) 1))
          (hyrolo-outline-hide-sublevels 2)
          (should (= (point) 1))
          (should (string-match-p
                   (concat "^\\(===+.*[^*]\\|" (regexp-quote "...\n") "\\)"
                           (regexp-quote "\
* h-org 1...
** h-org 1.1...
** h-org 1.2...
* h-org 2...
** h-org 2.1...
"
                                         ) "$")
                   (hyrolo-tests--outline-as-string)))

          ;; On first header - 2 levels
          (goto-char (point-min))
          (hyrolo-outline-show-all)
          (search-forward "* h-org 1")
          (beginning-of-line)
          (hyrolo-outline-hide-sublevels 2)
          (should (string= "\
* h-org 1...
** h-org 1.1...
** h-org 1.2...
* h-org 2...
** h-org 2.1...
"
                           (hyrolo-tests--outline-as-string (point))))

          ;; On second header - 2 levels
          (goto-char (point-min))
          (hyrolo-outline-show-all)
          (search-forward "** h-org 1.1")
          (beginning-of-line)
          (hyrolo-outline-hide-sublevels 2)
          (should (string= "\
** h-org 1.1...
** h-org 1.2...
* h-org 2...
** h-org 2.1...
"
                           (hyrolo-tests--outline-as-string (point)))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--hyrolo-outline-show-subtree ()
  "Verify `hyrolo-hyrolo-outline-show-subtree' shows everything after heading at deeper levels."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (hyrolo-file-list (list org-file1)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))

          ;; First line - show all
          (should (= (point) 1))
          (let ((original-look (hyrolo-tests--outline-as-string)))
            (hyrolo-outline-hide-subtree)
            (should (string-match-p
                     (concat "^===+" (regexp-quote "...") "\n$")
                     (hyrolo-tests--outline-as-string)))
            (hyrolo-outline-show-subtree)
            (should (string= original-look (hyrolo-tests--outline-as-string))))

          ;; On first header
          (goto-char (point-min))
          (hyrolo-outline-show-all)
          (hyrolo-outline-hide-sublevels 1)
          (search-forward "* h-org 1")
          (beginning-of-line)
          (let ((original-look (hyrolo-tests--outline-as-string)))
            (hyrolo-outline-show-subtree)
            (should (string= "\
* h-org 1
body
** h-org 1.1
body
** h-org 1.2
body
*** h-org 1.2.1
body
* h-org 2...
"
                             (hyrolo-tests--outline-as-string (point))))
            ;; Hide it again
            (hyrolo-outline-hide-subtree)
            (should (string= original-look (hyrolo-tests--outline-as-string))))

          ;; On second level header
          (goto-char (point-min))
          (hyrolo-outline-show-all)
          (hyrolo-outline-hide-sublevels 2)
          (search-forward "** h-org 1.2")
          (beginning-of-line)
          (let ((original-look (hyrolo-tests--outline-as-string)))
            (hyrolo-outline-show-subtree)
            (should (string= "\
** h-org 1.2
body
*** h-org 1.2.1
body
* h-org 2...
** h-org 2.1...
"
                             (hyrolo-tests--outline-as-string (point))))
            ;; Hide it again
            (hyrolo-outline-hide-subtree)
            (should (string= original-look (hyrolo-tests--outline-as-string)))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(defun hyrolo-tests--hyrolo-section-header (filename)
  "Return a hyrolo section header for FILENAME."
  (concat
   "===============================================================================\n@loc> \""
   filename
   "\"\n"
   "===============================================================================\n"))

(ert-deftest hyrolo-tests--hyrolo-reveal-mode ()
  "Verify hidden sections are shown when using {TAB} to move through matches."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (hyrolo-file-list (list org-file1)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))
          (hyrolo-top-level)

          (should (string=
                   (concat (hyrolo-tests--hyrolo-section-header org-file1)
                           "* h-org 1...\n* h-org 2...\n")
                   (hyrolo-tests--outline-as-string)))

          (execute-kbd-macro (kbd "TAB"))
          (should (string=
                   (concat (hyrolo-tests--hyrolo-section-header org-file1)
                           "* h-org 1\nbody\n** h-org 1.1\nbody\n** h-org 1.2\nbody\n*** h-org 1.2.1\nbody\n* h-org 2...\n")
                   (hyrolo-tests--outline-as-string)))

          (execute-kbd-macro (kbd "f TAB"))
          (should (string=
                   (concat (hyrolo-tests--hyrolo-section-header org-file1)
                           "* h-org 1\nbody\n** h-org 1.1\nbody\n** h-org 1.2\nbody\n*** h-org 1.2.1\nbody\n* h-org 2\nbody\n** h-org 2.1\nbody\n")
                   (hyrolo-tests--outline-as-string))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--hyrolo-reveal-mode-kotl-file ()
  "Verify hidden sections are shown when using {TAB} to move through matches in kotl."
  (let* ((kotl-file1 (hyrolo-tests--gen-kotl-outline "h-kotl" "body" 1))
         (hyrolo-file-list (list kotl-file1)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-grep "body")))
          (hyrolo-top-level)

          (should (string= (concat
                            (hyrolo-tests--hyrolo-section-header kotl-file1)
                            "\
   1. h-kotl...
")
                           (hyrolo-tests--outline-as-string)))
          (hyrolo-next-match)
          (should (string= (concat
                            (hyrolo-tests--hyrolo-section-header kotl-file1)
                            "\
   1. h-kotl
      body

     1a. h-kotl 1
         body 1
")
                           (hyrolo-tests--outline-as-string))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(defconst hyrolo-tests---org-expected-outline-for-top-level
  "\
* h-org 1...
* h-org 2...
"
  "Expected outline for org test data.")

(ert-deftest hyrolo-tests--top-level-outline-for-all-file-types ()
  "Verify `hyrolo-top-level' shows first level for all supported file types."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (otl-file1 (make-temp-file "hypb" nil ".otl" hyrolo-tests--outline-content-otl))
         (md-file1 (make-temp-file "hypb" nil ".md" hyrolo-tests--outline-content-md))
         (kotl-file1 (hyrolo-tests--gen-kotl-outline "h-kotl" "body" 2))
         (hyrolo-file-list (list org-file1 otl-file1 md-file1 kotl-file1)))
    (unwind-protect
        (progn
          (should (= 7 (hyrolo-grep "body")))
          (hyrolo-top-level)

          (should (string=
                   (concat (hyrolo-tests--hyrolo-section-header org-file1)
                           hyrolo-tests---org-expected-outline-for-top-level
                           (hyrolo-tests--hyrolo-section-header otl-file1)
                           (hyrolo-tests--modify-test-data "*" "otl" hyrolo-tests---org-expected-outline-for-top-level)
                           (hyrolo-tests--hyrolo-section-header md-file1)
                           (hyrolo-tests--modify-test-data "#" "md" hyrolo-tests---org-expected-outline-for-top-level)
                           (hyrolo-tests--hyrolo-section-header kotl-file1)
                           "\
   1. h-kotl...
")
                   (hyrolo-tests--outline-as-string))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(defconst hyrolo-tests---org-expected-outline-for-overview
  "\
* h-org 1...
** h-org 1.1...
** h-org 1.2...
*** h-org 1.2.1...
* h-org 2...
** h-org 2.1...
"
  "Expected outline for org test data.")

(ert-deftest hyrolo-tests--overview-outline-for-all-file-types ()
  "Verify `hyrolo-overview' shows all level headings for all supported file types."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (otl-file1 (make-temp-file "hypb" nil ".otl" hyrolo-tests--outline-content-otl))
         (md-file1 (make-temp-file "hypb" nil ".md" hyrolo-tests--outline-content-md))
         (kotl-file1 (hyrolo-tests--gen-kotl-outline "h-kotl" "body" 2))
         (hyrolo-file-list (list org-file1 otl-file1 md-file1 kotl-file1)))
    (unwind-protect
        (progn
          (should (= 7 (hyrolo-grep "body")))
          (hyrolo-overview nil)

          (should (string=
                   (concat (hyrolo-tests--hyrolo-section-header org-file1)
                           hyrolo-tests---org-expected-outline-for-overview
                           (hyrolo-tests--hyrolo-section-header otl-file1)
                           (hyrolo-tests--modify-test-data "*" "otl" hyrolo-tests---org-expected-outline-for-overview)
                           (hyrolo-tests--hyrolo-section-header md-file1)
                           (hyrolo-tests--modify-test-data "#" "md" hyrolo-tests---org-expected-outline-for-overview)
                           (hyrolo-tests--hyrolo-section-header kotl-file1)
                           "\
   1. h-kotl...
     1a. h-kotl 1...
       1a1. h-kotl 2...
")
                   (hyrolo-tests--outline-as-string))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--mail-to ()
  "Verify composing mail works."
  (let* ((org-file (make-temp-file "hypb" nil ".org" "\
* h-org 1
<first@receiver.org>
* h-org 2
<second@receiver.org>
"))
         (hyrolo-file-list (list org-file)))
    (unwind-protect
        (let ((hypb:mail-address-mode-list '(hyrolo-mode)))
          (should (= 2 (hyrolo-grep "receiver\\.org")))
          (mocklet (((actypes::link-to-compose-mail "first@receiver.org") => t))
            (hyrolo-mail-to))
          (forward-line)
          (mocklet (((actypes::link-to-compose-mail "second@receiver.org") => t))
            (hyrolo-mail-to)))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list)))
  (with-temp-buffer
    (mocklet (((beep) => t))
      (ert-with-message-capture cap
        (hyrolo-mail-to)
        (hy-test-helpers:should-last-message "Invalid buffer or no e-mail address found" cap)))))

(ert-deftest hyrolo-tests--location-movement ()
  "Verify movement between location headers."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (otl-file1 (make-temp-file "hypb" nil ".otl" hyrolo-tests--outline-content-otl))
         (hyrolo-file-list (list org-file1 otl-file1)))
    (unwind-protect
        (progn
          (should (= 4 (hyrolo-grep "body")))
          (hyrolo-to-next-loc)
          (should (looking-at-p (concat "@loc> \"" org-file1 "\"")))
          (hyrolo-to-next-loc)
          (should (looking-at-p (concat "@loc> \"" otl-file1 "\"")))
          (hyrolo-to-previous-loc)
          (should (looking-at-p (concat "@loc> \"" org-file1 "\""))))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--goto-org-body-match ()
  "Move from body match to target at org file."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (hyrolo-file-list (list org-file1)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "body")))
          (hyrolo-next-match)
          (action-key)
          (should (string= (hypb:buffer-file-name) org-file1))
          (should (looking-at-p "body")))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--goto-org-header-match ()
  "Move from heading match to target at org file."
  (let* ((org-file1 (make-temp-file "hypb" nil ".org" hyrolo-tests--outline-content-org))
         (hyrolo-file-list (list org-file1)))
    (unwind-protect
        (progn
          (should (= 2 (hyrolo-grep "h-org")))
          (hyrolo-next-match)
          (action-key)
          (should (string= (hypb:buffer-file-name) org-file1))
          (should (looking-at-p "h-org 1$")))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--goto-kotl-body-match ()
  "Move from body match to target at kotl file."
  (let* ((kotl-file1 (hyrolo-tests--gen-kotl-outline "h-kotl" "body" 1))
         (hyrolo-file-list (list kotl-file1)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-grep "body")))
          (hyrolo-next-match)
          (action-key)
          (should (string= (hypb:buffer-file-name) kotl-file1))
          (should (looking-at-p "body$"))
          (should (string= (buffer-substring-no-properties (point-min) (point-max))
                           "\
   1. h-kotl
      body

     1a. h-kotl 1
         body 1

"                           )))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--goto-kotl-header-match ()
  "Move from heading match to target at first line in kotl file."
  (let* ((kotl-file1 (hyrolo-tests--gen-kotl-outline "h-kotl" "body" 1))
         (hyrolo-file-list (list kotl-file1)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-grep "h-kotl")))
          (hyrolo-next-match)
          (action-key)
          (should (string= (hypb:buffer-file-name) kotl-file1))
          (should (looking-at-p "h-kotl$"))
          (should (string= (buffer-substring-no-properties (point-min) (point-max))
                           "\
   1. h-kotl
      body

     1a. h-kotl 1
         body 1

"                           )))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--goto-kotl-body-with-slash-match ()
  "Move from body match to target line with slash in kotl file."
  (let* ((kotl-file1 (hyrolo-tests--gen-kotl-outline "h-kotl" "body1 / body2" 1))
         (hyrolo-file-list (list kotl-file1)))
    (unwind-protect
        (progn
          (should (= 1 (hyrolo-grep "body2")))
          (hyrolo-next-match)
          (action-key)
          (should (string= (hypb:buffer-file-name) kotl-file1))
          (should (looking-at-p "body2$"))
          (should (string= (buffer-substring-no-properties (point-min) (point-max))
                           "\
   1. h-kotl
      body1 / body2

     1a. h-kotl 1
         body1 / body2 1

"                           )))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--goto-kotl-header-with-slash-match ()
  "Move from heading match to target line with a slash in kotl file."
  (let* ((kotl-file1 (hyrolo-tests--gen-kotl-outline "h1 / h2" "body" 1))
         (hyrolo-file-list (list kotl-file1)))
    (unwind-protect
        (progn
	  (kotl-mode:beginning-of-buffer)
          (should (= 1 (hyrolo-grep "h2")))
          (action-key)
          (should (string= (hypb:buffer-file-name) kotl-file1))
          (should (looking-at-p "h1 / h2$"))
          (should (string= (buffer-substring-no-properties (point-min) (point-max))
                           "\
   1. h1 / h2
      body

     1a. h1 / h2 1
         body 1

"                           )))
      (kill-buffer hyrolo-display-buffer)
      (hy-delete-files-and-buffers hyrolo-file-list))))

(ert-deftest hyrolo-tests--grep-count ()
  "Verify number of matched entries are correct."
  (unwind-protect
      (with-temp-buffer
        (org-mode)
        (insert "\
* match
match
* match
other
* other
match
")
        ;; Count number of entries that have a match
        (should (= (hyrolo-grep "match" nil (current-buffer) t nil) 3))
        ;; Count number of entries that only match on the first line
        (should (= (hyrolo-grep "match" nil (current-buffer) t t) 2))
        ;; Count max number of entries
        (should (= (hyrolo-grep "match" 1 (current-buffer) t nil) 1))
        ;; Nothing if there is no match
        (should (= (hyrolo-grep "nothing" nil (current-buffer) t nil) 0)))
    (and (get-buffer hyrolo-display-buffer)
         (kill-buffer hyrolo-display-buffer)
         (ert-fail "Buffer %s should not have been created" hyrolo-display-buffer))))

(ert-deftest hyrolo-tests--expand-path-list ()
  "Verify `hyrolo-expand-path-list'."
  (let ((hyrolo-default-file "~/default"))
    (should (equal (hyrolo-expand-path-list nil)
		   (list hyrolo-default-file)))
    (defvar bbdb-file)
    (defvar google-contacts-buffer-name)
    (let ((bbdb-file "~/bbdb")
          (google-contacts-buffer-name "*Google Contacts*"))
      (mocklet (((hyrolo-google-contacts-p) => t))
        (should (equal (hyrolo-expand-path-list nil)
		       (list hyrolo-default-file bbdb-file google-contacts-buffer-name))))))
  (mocklet (((hpath:expand-list
              '("/file1")
              "\\.\\(kotl?\\|org\\|ou?tl\\|md\\|markdown\\|mkd\\|mdown\\|mkdn\\|mdwn\\)$")
             => (list "/file1")))
    (should (equal (hyrolo-expand-path-list '("/file1")) '("/file1")))))

(ert-deftest hyrolo-tests--at-tags-p ()
  "Verify `hyrolo-at-tags-p´."
  (let ((orig-buffer-name (symbol-function 'buffer-name)))
    (cl-letf (((symbol-function 'buffer-name)
               (lambda (&optional buffer)
                 (if (string-prefix-p " *temp*" (funcall orig-buffer-name))
                     "*HyRolo*"
                   (funcall orig-buffer-name)))))
      (with-temp-buffer
        (should (hyrolo-at-tags-p t)))
      (with-temp-buffer
        (insert "* header  :tag1:tag2:\n")
        (goto-char (point-min))
        (search-forward ":tag1")
        (should (hyrolo-at-tags-p)))))
  (with-temp-buffer
    (org-mode)
    (mocklet (((hyrolo-get-file-list) => '("file")))
      (let ((buffer-file-name "file"))
        (should (hyrolo-at-tags-p t))))))

(ert-deftest hyrolo-tests--display-matches ()
  "Verify hyrolo-display-matches displays the buffer with the matches."
  (ert-info ("No previous search nor buffer is provided")
    (with-mock
      (mock (get-buffer hyrolo-display-buffer) => nil)
      (should-error (hyrolo-display-matches nil))))
  (with-temp-buffer
    (let ((buf1 (current-buffer)))
      (with-temp-buffer
        (let ((buf2 (current-buffer))
              hyrolo--wconfig
              (display-buffer-alist '((".*" display-buffer-same-window))))
          (ert-info ("Provided buffer is selected with window config")
            (hyrolo-display-matches buf1)
            (should hyrolo--wconfig)
            (should (equal buf1 (current-buffer))))
          (ert-info ("Provided buffer already visible, don't save window config")
            (setq hyrolo--wconfig nil)
            (hyrolo-display-matches buf1)
            (should-not hyrolo--wconfig))
          (ert-info ("return-to-buffer is selected")
            (hyrolo-display-matches buf1 buf2)
            (should (equal buf2 (current-buffer)))))))))

(ert-deftest hyrolo-tests--consult-fgrep ()
  "Verify `hyrolo-consult-grep' calls hsys-consult-grep."
  (with-mock
    (mock (hyrolo-consult-grep (regexp-quote "string") 0 nil nil))
    (hyrolo-consult-fgrep "string" 0))
  (with-mock
    ;; Ignore checking grep-includes and ripgrep-globs
    (mock (hsys-consult-grep * * "string" 1 (list "path-list") "Fgrep HyRolo files"))
    (hyrolo-consult-fgrep "string" 1 (list "path-list"))))

(ert-deftest hyrolo-tests--consult-grep ()
  "Verify `hyrolo-consult-grep' calls hsys-consult-grep."
  (defvar hyrolo-file-list)
  (defvar path-list)
  (let ((hyrolo-file-list '("hyrolo.otl" "another.otl"))
        (path-list '("yet-another.otl")))
    (with-mock
      ;; Ignore checking grep-includes and ripgrep-globs
      (mock (hsys-consult-grep * * nil nil hyrolo-file-list "Grep HyRolo files") => t)
      (call-interactively #'hyrolo-consult-grep))
    (with-mock
      ;; Ignore checking grep-includes and ripgrep-globs
      (mock (hsys-consult-grep * * "regexp" 0 path-list "Grep HyRolo headlines") => t)
      (hyrolo-consult-grep "regexp" 0 path-list))))

(ert-deftest hyrolo-tests--edit ()
  "Verify `hyrolo-edit'."
  (defvar hyrolo-file)
  (let ((hyrolo-file (make-temp-file "hypb" nil ".otl" "===\nHdr\n===\n"))
        (item-pat (rx "*" (= 3 space) "item")))
    (unwind-protect
        (let ((hyrolo-file-list (list hyrolo-file)))
          (find-file (car (hyrolo-get-file-list)))
          (hyrolo-add "alpha")
          (hyrolo-add "item")
          (hyrolo-add "xerxes")

          (ert-info ("Edit item")
            (goto-char (point-min))
            (hyrolo-edit "item" hyrolo-file)
            (should (looking-at-p item-pat)))

          (ert-info ("Edit item called interactively")
            (goto-char (point-min))
            (with-mock
              (mock (hsys-consult-grep-headlines-read-regexp #'hyrolo-consult-grep *) => "item")
              (call-interactively #'hyrolo-edit))
            (should (looking-at-p item-pat)))

          (ert-info ("Edit item called interactively with consult active")
            (goto-char (point-min))
            (with-mock
              (mock (hsys-consult-grep-headlines-read-regexp #'hyrolo-consult-grep *) =>
                    (concat hyrolo-file ":10:item"))
              (mock (hsys-consult-active-p) => t)
              (call-interactively #'hyrolo-edit))
            (should (looking-at-p item-pat)))

          (ert-info ("Verify hook is called")
            (let* (run-hook (hook (lambda () (setq run-hook t))))
              (unwind-protect
                  (progn
                    (add-hook 'hyrolo-edit-hook hook)
                    (hyrolo-edit "item" hyrolo-file)
                    (should run-hook))
                (remove-hook 'hyrolo-edit-hook hook))))

          (ert-info ("Error cases")
            (should-error (hyrolo-edit 'symbol))
            (should-error (hyrolo-edit "" (make-temp-name "hypb")))
            (should-not (hyrolo-edit nil))
            (with-mock
              (mock (beep) => t)
              (ert-with-message-capture cap
                (should-not (hyrolo-edit "beta" hyrolo-file))
                (hy-test-helpers:should-last-message "not found" cap)))))

      (hy-delete-file-and-buffer hyrolo-file))))

(ert-deftest hyrolo-tests--grep-or-fgrep ()
  "Verify `hyrolo-grep-or-fgrep' calls hyrolo grep or fgrep based on prefix arg.
Uses mocks to verify the call path."
  (with-mock
    (mock (call-interactively 'hyrolo-grep))
    (hyrolo-grep-or-fgrep))
  (with-mock
    (mock (call-interactively 'hyrolo-fgrep))
    (hyrolo-grep-or-fgrep '(4))))

(ert-deftest hyrolo-tests--kill ()
  "Verify `hyrolo-kill' deletes hyrolo items and handles error cases.
Dependencies on the consult package are mocked."
  (defvar hyrolo-file)
  (let ((hyrolo-file (make-temp-file "hypb" nil ".otl" "===\nHdr\n===\n"))
        (item-pat (rx bol "*" (= 3 space) "item")))
    (unwind-protect
        (let ((hyrolo-file-list (list hyrolo-file)))
          (find-file (car (hyrolo-get-file-list)))
          (hyrolo-add "alpha")
          (hyrolo-add "item")
          (hyrolo-add "xerxes")
          (should (= 1 (count-matches item-pat (point-min) (point-max))))

          (ert-info ("Kill item")
            (goto-char (point-min))
            (should-not (hyrolo-kill "not-exists" hyrolo-file))
            (should (hyrolo-kill "item" hyrolo-file))
            (should (= 0 (count-matches item-pat (point-min) (point-max)))))

          (ert-info ("Kill item called interactively")
            (hyrolo-add "item")
            (goto-char (point-min))
            (with-mock
              (mock (hsys-consult-grep-headlines-read-regexp #'hyrolo-consult-grep *) => "item")
              (stub y-or-n-p => t)
              (should (call-interactively #'hyrolo-kill)))
            (should (= 0 (count-matches item-pat (point-min) (point-max)))))

          (ert-info ("Kill item called interactively with consult active")
            (hyrolo-add "item")
            (goto-char (point-min))
            (with-mock
              (mock (hsys-consult-grep-headlines-read-regexp #'hyrolo-consult-grep *) =>
                    (concat hyrolo-file ":10:item"))
              (mock (hsys-consult-active-p) => t)
              (stub y-or-n-p => t)
              (call-interactively #'hyrolo-kill))
            (should (= 0 (count-matches item-pat (point-min) (point-max)))))

          (ert-info ("Error cases")
            (should-error (hyrolo-kill 'symbol))
            (should-error (hyrolo-kill "" (make-temp-name "hypb")))
            (should-error (hyrolo-kill nil))
            (with-mock
              (mock (beep) => t)
              (ert-with-message-capture cap
                (should-not (hyrolo-kill "not-exist" hyrolo-file))
                (hy-test-helpers:should-last-message "not found" cap)))))
      (hy-delete-file-and-buffer hyrolo-file))))

(ert-deftest hyrolo-tests--set-date ()
  "Verify `hyrolo-set-date' sets and updates an items date."
  (let ((test-time "2024-12-11"))
    (cl-letf (((symbol-function 'format-time-string)
               (lambda (_fmt &optional _time _zone) test-time)))
      (with-temp-buffer
        (insert "* item\n")

        (ert-info ("Do not insert date if date format is empty or nil")
          (dolist (v '("" nil))
            (let ((hyrolo-date-format v))
              (goto-char (point-min))
              (hyrolo-set-date)
              (should (string= "* item\n" (buffer-string))))))

        (ert-info ("Do not insert date in kotl-mode")
          (let ((major-mode 'kotl-mode))
            (goto-char (point-min))
            (hyrolo-set-date)
            (should (string= "* item\n" (buffer-string)))))

        (ert-info ("Don't set date if date is not present and request is edit-only")
          (goto-char (point-min))
          (hyrolo-set-date t)
          (should (string= "* item\n" (buffer-string))))

        (ert-info ("Set date if not there")
          (goto-char (point-min))
          (hyrolo-set-date)
          (should (string= "* item\n\t2024-12-11\n" (buffer-string))))

        (ert-info ("Update date if item has a date")
          (setq test-time "2025-01-01")
          (goto-char (point-min))
          (hyrolo-set-date)
          (should (string= "* item\n\t2025-01-01\n" (buffer-string))))

        (ert-info ("Update date if item has a date, keep trailing newlines")
          (setq test-time "2025-01-02")
          (goto-char (point-max))
          (insert "\n\n")
          (goto-char (point-min))
          (hyrolo-set-date)
          (should (string= "* item\n\t2025-01-02\n\n\n" (buffer-string))))

        (ert-info ("Update date if item has a date, keep leading whitespace")
          (goto-char (point-min))
          (search-forward "2025-01-02")
          (beginning-of-line)
          (insert "\t\t")
          (setq test-time "2025-01-03")
          (goto-char (point-min))
          (hyrolo-set-date)
          (should (string= "* item\n\t\t\t2025-01-03\n\n\n" (buffer-string))))))))

(ert-deftest hyrolo-tests--rename ()
  "Verify `hyrolo-rename' changes the name of the old rolo file."
  (defvar new-file)
  (let* ((old-file (make-temp-file "hypb" nil ".otl" "old-file"))
         (old-file-backup (concat old-file "~"))
         (new-file (concat (expand-file-name (make-temp-name "hypb") "/tmp") ".otl"))
         (new-file-backup (concat new-file "~")))
    (unwind-protect
        (with-mock
          (mock (hyrolo-get-file-list) => (list new-file))
          (mock (hyrolo-prompt 'y-or-n-p *) => t)
          (make-empty-file old-file-backup)
          (find-file-noselect old-file)
          (ert-with-message-capture cap
            (hyrolo-rename old-file new-file)
            (should (string-search "Your personal rolo file is now" cap)))
          (should (file-exists-p new-file-backup))
          (with-current-buffer (find-file new-file)
            (should (looking-at-p "old-file"))))
      (hy-delete-files-and-buffers (list old-file old-file-backup new-file new-file-backup)))))

(provide 'hyrolo-tests)

;; This file can't be byte-compiled without the `el-mock' package
;; which is not a dependency of Hyperbole.
;;
;; Local Variables:
;; no-byte-compile: t
;; End:

;;; hyrolo-tests.el ends here
