;;; core-spacebind-utest.el --- Core Unit Test File -*- lexical-binding: t -*-
;;
;; Copyright (c) 2012-2022 Sylvain Benner & Contributors
;;
;; Author: Eugene "JAremko" Yaremenko <w3techplayground@gmail.com>
;; URL: https://github.com/syl20bnr/spacemacs
;;
;; This file is not part of GNU Emacs.
;;
;; 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 <http://www.gnu.org/licenses/>.


(require 'core-spacebind)
(require 'cl-lib)

;;;; Helpers
(defun test-spacebind/plist-diff (plist1 plist2)
  "Returns difference of PLIST1 and PLIST2 using `equal'."
  (cl-labels ((recur
               (acc plist)
               (let* ((key (car plist))
                      (val (cadr plist))
                      (rest (cddr plist))
                      (pair (list key val))
                      (new-acc (append (list pair) acc)))
                 (if rest
                     (recur new-acc rest)
                   new-acc))))
    (let ((set-1 (recur '() plist1))
          (set-2 (recur '() plist2)))
      (cl-set-exclusive-or set-1 set-2 :test 'equal))))

(defmacro test-spacebind|log-stack-eval (&rest body)
  "Evaluate BODY while mocking `spacebind//process-bind-stack'.
Stack values after the evaluation are returned and the stacks cleaned.

The return value is a plist of the shape:

(:minor-mode-replacements <STACK>
 :major-mode-replacements <STACK>
 :declare-prefix <STACK>
 :declare-prefix-for-mode <STACK>
 :set-leader-keys <STACK>
 :set-leader-keys-for-major-mode <STACK>
 :set-leader-keys-for-minor-mode <STACK>
 :global-replacements <STACK>
 :fn-key-seq-override <STACK>)

<STACK> is a corresponding binding stack.

NOTE: `spacebind--eager-bind' set to true."
  `(let ((spacebind--eager-bind t)
         (ret-plist nil))
     (cl-labels ((spacebind//process-bind-stack
                  ()
                  (setq ret-plist (list
                                   :minor-mode-replacements
                                   spacebind--bs-minor-mode-replacements
                                   :major-mode-replacements
                                   spacebind--bs-major-mode-replacements
                                   :declare-prefix
                                   spacebind--bs-declare-prefix
                                   :declare-prefix-for-mode
                                   spacebind--bs-declare-prefix-for-mode
                                   :set-leader-keys
                                   spacebind--bs-leader-keys
                                   :set-leader-keys-for-major-mode
                                   spacebind--bs-leader-keys-for-major-mode
                                   :set-leader-keys-for-minor-mode
                                   spacebind--bs-leader-keys-for-minor-mode
                                   :global-replacements
                                   spacebind--bs-global-replacements
                                   :fn-key-seq-override
                                   spacebind--bs-fn-key-seq-override)
                        spacebind--bs-global-replacements nil
                        spacebind--bs-leader-keys-for-minor-mode nil
                        spacebind--bs-leader-keys-for-major-mode nil
                        spacebind--bs-leader-keys nil
                        spacebind--bs-declare-prefix-for-mode nil
                        spacebind--bs-declare-prefix nil
                        spacebind--bs-major-mode-replacements nil
                        spacebind--bs-minor-mode-replacements nil
                        spacebind--bs-fn-key-seq-override nil
                        spacebind--timer [t])))
       (progn
         ,@body
         ret-plist))))

;; Example:
(thread-last (spacemacs|spacebind
              :major
              (major-foo-mode
               ("a" "section a"
                ("a" foo-fn "execute foo-fn"))))
  (test-spacebind|log-stack-eval)
  (format "%S")
  (insert)
  ;; Prevents execution
  (declare))

(defmacro test-spacebind|validate-keys (&rest body)
  "Mocks `spacebind//process-bind-stack' and validate key sequences.
NOTE: `spacebind--eager-bind' set to true. "
  `(cl-letf* ((invalid-key-seqs '())
              (spacebind--eager-bind t)
              (spacebind--bs-minor-mode-replacements '())
              (spacebind--bs-major-mode-replacements '())
              (spacebind--bs-declare-prefix '())
              (spacebind--bs-declare-prefix-for-mode '())
              (spacebind--bs-leader-keys '())
              (spacebind--bs-leader-keys-for-major-mode '())
              (spacebind--bs-leader-keys-for-minor-mode '())
              (spacebind--bs-global-replacements '())
              (spacebind--bs-fn-key-seq-override '())
              (spacebind--timer [t])
              (called nil)
              ((symbol-function 'spacebind//process-bind-stack)
               (lambda () (progn))))
     ,@body
     (dolist (el (apply 'append
                        (append spacebind--bs-minor-mode-replacements
                                spacebind--bs-major-mode-replacements
                                spacebind--bs-declare-prefix
                                spacebind--bs-declare-prefix-for-mode
                                spacebind--bs-leader-keys
                                spacebind--bs-leader-keys-for-major-mode
                                spacebind--bs-leader-keys-for-minor-mode
                                spacebind--bs-global-replacements
                                spacebind--bs-fn-key-seq-override)))
       (when (listp el) ;; all list arguments are key sequences.
         (condition-case err (kbd (string-join el " "))
           ((error nil) (push err invalid-key-seqs)))))
     (cl-remove-duplicates invalid-key-seqs :test 'equal)))

;;;; Tests:
(ert-deftest test-spacebind-major-mode-always-generates-right-stack ()
  (thread-last (spacemacs|spacebind
                :major
                (py-mode
                 "with a description"
                 ("c" "compile/execute"
                  ("c" spacemacs/python-execute-file "execute file"))))
    (test-spacebind|log-stack-eval)
    (test-spacebind/plist-diff
     '(:major-mode-replacements
       ((py-mode ("c" "c") "execute file"))
       :declare-prefix-for-mode
       ((py-mode ("c") "compile/execute"))
       :set-leader-keys-for-major-mode
       ((py-mode ("c" "c") spacemacs/python-execute-file))
       :minor-mode-replacements nil
       :declare-prefix nil
       :set-leader-keys nil
       :set-leader-keys-for-minor-mode nil
       :fn-key-seq-override nil
       :global-replacements nil))
    (eq nil)
    (should)))

(ert-deftest test-spacebind-minor-mode-always-generates-right-stack ()
  (thread-last (spacemacs|spacebind
                :minor
                (foo-mode
                 "With a description"
                 ("a" "section under a key"
                  ("b" baz-fn "call baz-fn"))))
    (test-spacebind|log-stack-eval)
    (test-spacebind/plist-diff
     '(:minor-mode-replacements
       ((foo-mode ("a" "b") "call baz-fn"))
       :set-leader-keys-for-minor-mode
       ((foo-mode ("a" "b") baz-fn))
       :declare-prefix-for-mode
       ((foo-mode ("a") "section under a key"))
       :major-mode-replacements nil
       :declare-prefix nil
       :set-leader-keys nil
       :set-leader-keys-for-major-mode nil
       :fn-key-seq-override nil
       :global-replacements nil))
    (eq nil)
    (should)))

(ert-deftest test-spacebind-global-always-generates-right-stack ()
  (thread-last (spacemacs|spacebind
                "With a description"
                :global
                (("a" "section under a key"
                  ("b" bar-fn "call bar-fn"))))
    (test-spacebind|log-stack-eval)
    (test-spacebind/plist-diff
     '(:declare-prefix
       ((("a") "section under a key"))
       :set-leader-keys
       ((("a" "b") bar-fn))
       :global-replacements
       ((("a" "b") "call bar-fn"))
       :fn-key-seq-override nil
       :minor-mode-replacements nil
       :major-mode-replacements nil
       :declare-prefix-for-mode nil
       :set-leader-keys-for-major-mode nil
       :set-leader-keys-for-minor-mode nil))
    (eq nil)
    (should)))

(ert-deftest test-spacebind-doc-string-always-ignored ()
  (should (equal (test-spacebind|log-stack-eval
                  (spacemacs|spacebind
                   :major
                   (py-mode
                    "With a doc-string"
                    ("c" "compile/execute"
                     ("c" spacemacs/python-execute-file "execute file")))))
                 (test-spacebind|log-stack-eval
                  (spacemacs|spacebind
                   :major
                   (py-mode
                    ("c" "compile/execute"
                     ("c" spacemacs/python-execute-file "execute file")))))))
  (should (equal (test-spacebind|log-stack-eval
                  (spacemacs|spacebind
                   :minor
                   (foo-mode
                    "With a doc-string"
                    ("a" "section under a key"
                     ("b" bar-fn "call bar-fn")))
                   (baz-mode
                    "With a doc-string"
                    ("c" "section under a key"
                     ("d" qux-fn "call qux-fn")))))
                 (test-spacebind|log-stack-eval
                  (spacemacs|spacebind
                   :minor
                   (foo-mode
                    ("a" "section under a key"
                     ("b" bar-fn "call bar-fn")))
                   (baz-mode
                    ("c" "section under a key"
                     ("d" qux-fn "call qux-fn")))))))
  (should (equal (test-spacebind|log-stack-eval
                  (spacemacs|spacebind
                   :global
                   ("With a doc-string"
                    ("a" "section under a key"
                     ("b" bar-fn "call bar-fn")))))
                 (test-spacebind|log-stack-eval
                  (spacemacs|spacebind
                   :global
                   (("a" "section under a key"
                     ("b" bar-fn "call bar-fn"))))))))

(ert-deftest test-spacebind-desc-overrides-always-applied ()
  (thread-first (spacemacs|spacebind
                 :global
                 (("a" foo-fn ("ignored(used for docs)" :label "used label"))))
    (test-spacebind|log-stack-eval)
    (plist-get :global-replacements)
    (car)
    (equal '(("a") "used label"))
    (should))
  (thread-first (spacemacs|spacebind
                 :minor
                 (foo-mode
                  ("a" foo-fn ("ignored(used for docs)" :label "used label"))))
    (test-spacebind|log-stack-eval)
    (plist-get :minor-mode-replacements)
    (car)
    (equal '(foo-mode ("a") "used label"))
    (should))
  (thread-first (spacemacs|spacebind
                 :major
                 (foo-mode
                  ("a" foo-fn ("ignored(used for docs)" :label "used label"))))
    (test-spacebind|log-stack-eval)
    (plist-get :major-mode-replacements)
    (car)
    (equal '(foo-mode ("a") "used label"))
    (should)))

(ert-deftest test-spacebind-key-overrides-always-applied ()
  (thread-first (spacemacs|spacebind
                 :global
                 ((("a" :label "press a") foo-fn "label")))
    (test-spacebind|log-stack-eval)
    (plist-get :fn-key-seq-override)
    (car)
    (equal '("foo-fn" "press a" "label"))
    (should))
  (thread-first (spacemacs|spacebind
                 :minor
                 (foo-mode
                  (("a" :label "press a") foo-fn "label")))
    (test-spacebind|log-stack-eval)
    (plist-get :fn-key-seq-override)
    (car)
    (equal '("foo-fn" "press a" "label"))
    (should))
  (thread-first (spacemacs|spacebind
                 :major
                 (foo-mode
                  (("a" :label "press a") foo-fn "label")))
    (test-spacebind|log-stack-eval)
    (plist-get :fn-key-seq-override)
    (car)
    (equal '("foo-fn" "press a" "label"))
    (should)))

(ert-deftest test-spacebind-labels-multi-line-strings-always-joined ()
  (thread-first (spacemacs|spacebind
                 :global
                 (("a" foo-fn "This is a
                               multi line string")))
    (test-spacebind|log-stack-eval)
    (plist-get :global-replacements)
    (car)
    (equal '(("a") "This is a multi line string"))
    (should))
  (thread-first (spacemacs|spacebind
                 :minor
                 (foo-mode
                  ("a" foo-fn "This is a
                               multi line string")))
    (test-spacebind|log-stack-eval)
    (plist-get :minor-mode-replacements)
    (car)
    (equal '(foo-mode ("a") "This is a multi line string"))
    (should))
  (thread-first (spacemacs|spacebind
                 :major
                 (foo-mode
                  ("a" foo-fn "This is a
                               multi line string")))
    (test-spacebind|log-stack-eval)
    (plist-get :major-mode-replacements)
    (car)
    (equal '(foo-mode ("a") "This is a multi line string"))
    (should))
  (thread-first (spacemacs|spacebind
                 :global
                 (("a" foo-fn ("ignored" :label "This is a
                                                 multi line string"))))
    (test-spacebind|log-stack-eval)
    (plist-get :global-replacements)
    (car)
    (equal '(("a") "This is a multi line string"))
    (should)))

(ert-deftest test-spacebind-labels-pipe-slicing-always-works ()
  (thread-first (spacemacs|spacebind
                 :global
                 (("a" foo-fn "this part goes into label | that part omitted")))
    (test-spacebind|log-stack-eval)
    (plist-get :global-replacements)
    (car)
    (equal '(("a") "this part goes into label"))
    (should))
  (thread-first (spacemacs|spacebind
                 :minor
                 (foo-mode
                  ("a" foo-fn "this part goes into label | that part omitted")))
    (test-spacebind|log-stack-eval)
    (plist-get :minor-mode-replacements)
    (car)
    (equal '(foo-mode ("a") "this part goes into label"))
    (should))
  (thread-first (spacemacs|spacebind
                 :major
                 (foo-mode
                  ("a" foo-fn "this part goes into label | that part omitted")))
    (test-spacebind|log-stack-eval)
    (plist-get :major-mode-replacements)
    (car)
    (equal '(foo-mode ("a") "this part goes into label"))
    (should))
  (thread-first (spacemacs|spacebind
                 :global
                 (("a" foo-fn ("ignored"
                               :label "this part goes into label
                                       | that part omitted"))))
    (test-spacebind|log-stack-eval)
    (plist-get :global-replacements)
    (car)
    (equal '(("a") "this part goes into label"))
    (should)))

(ert-deftest test-spacebind-always-generates-right-stack ()
  (thread-last
      (spacemacs|spacebind
       :major
       (py-mode
        "Docstring for documentation"
        ("C-p" "compile/execute"
         ("TAB" spacemacs/python-execute-file "execute file")
         ("C" spacemacs/python-execute-file-focus "execute file and focus"))
        ("d" "debug"
         (("b" :label "->b") spacemacs/python-toggle-breakpoint "toggle
                                                                 breakpoint"))
        ("r" "refactor"
         ("i" spacemacs/python-remove-unused-imports "remove unused import"))
        ("s" "REPL"
         ("s" spacemacs/python-shell-send-buffer-switch
          "send buffer to REPL and focus | on the buffer")
         ("S" python-shell-send-buffer
          ("send buffer to REPL" :label "buffer -> REPL | without
                                                          focusing"))
         ("d" spacemacs/python-shell-send-defun-switch
          "send function around point to REPL and focus")
         ("D" python-shell-send-defun
          "send function around point to REPL")
         ("r" spacemacs/python-shell-send-region-switch
          "send region to REPL and focus")
         ("R" python-shell-send-region "send region to REPL")))
       :minor
       (some-minor-mode
        ("a" "section under a key"
         ("b" foo-fn "call foo-fn")
         ("c" "sub section under c key"
          ("d" "sub sub section under d key"
           ("e" baz-fn "call baz-fn")))))
       (some-another-minor-mode
        ("a" "section under a key"
         ("b" "sub section under b key"
          ("c" "sub sub section under c key"
           ("b" baz-fn "call baz-fn")))))
       :global
       (("C-v" "section under a key"
         ("b" bar-fn "call bar-fn"))))
    (test-spacebind|log-stack-eval)
    (test-spacebind/plist-diff
     '(:minor-mode-replacements
       ((some-another-minor-mode ("a" "b" "c" "b") "call baz-fn")
        (some-minor-mode ("a" "c" "d" "e") "call baz-fn")
        (some-minor-mode ("a" "b") "call foo-fn"))
       :major-mode-replacements
       ((py-mode ("s" "R") "send region to REPL")
        (py-mode ("s" "r") "send region to REPL and focus")
        (py-mode ("s" "D") "send function around point to REPL")
        (py-mode ("s" "d") "send function around point to REPL and focus")
        (py-mode ("s" "S") "buffer -> REPL")
        (py-mode ("s" "s") "send buffer to REPL and focus")
        (py-mode ("r" "i") "remove unused import")
        (py-mode ("d" "b") "toggle breakpoint")
        (py-mode ("C-p" "C") "execute file and focus")
        (py-mode ("C-p" "TAB") "execute file"))
       :declare-prefix
       ((("C-v") "section under a key"))
       :declare-prefix-for-mode
       ((some-another-minor-mode ("a" "b" "c") "sub sub section under c key")
        (some-another-minor-mode ("a" "b") "sub section under b key")
        (some-another-minor-mode ("a") "section under a key")
        (some-minor-mode ("a" "c" "d") "sub sub section under d key")
        (some-minor-mode ("a" "c") "sub section under c key")
        (some-minor-mode ("a") "section under a key")
        (py-mode ("s") "REPL") (py-mode ("r") "refactor")
        (py-mode ("d") "debug") (py-mode ("C-p") "compile/execute"))
       :set-leader-keys
       ((("C-v" "b") bar-fn))
       :set-leader-keys-for-major-mode
       ((py-mode ("s" "R") python-shell-send-region)
        (py-mode ("s" "r") spacemacs/python-shell-send-region-switch)
        (py-mode ("s" "D") python-shell-send-defun)
        (py-mode ("s" "d") spacemacs/python-shell-send-defun-switch)
        (py-mode ("s" "S") python-shell-send-buffer)
        (py-mode ("s" "s") spacemacs/python-shell-send-buffer-switch)
        (py-mode ("r" "i") spacemacs/python-remove-unused-imports)
        (py-mode ("d" "b") spacemacs/python-toggle-breakpoint)
        (py-mode ("C-p" "C") spacemacs/python-execute-file-focus)
        (py-mode ("C-p" "TAB") spacemacs/python-execute-file))
       :set-leader-keys-for-minor-mode
       ((some-another-minor-mode ("a" "b" "c" "b") baz-fn)
        (some-minor-mode ("a" "c" "d" "e") baz-fn)
        (some-minor-mode ("a" "b") foo-fn))
       :global-replacements
       ((("C-v" "b") "call bar-fn"))
       :fn-key-seq-override
       (("spacemacs/python-toggle-breakpoint" "->b" "toggle breakpoint"))))
    (eq nil)
    (should)))

(ert-deftest test-spacebind-always-generate-valid-key-seqs ()
  (thread-first
      (spacemacs|spacebind
       :major
       (py-mode
        "Docstring for documentation"
        ("C-p" "compile/execute"
         ("TAB" spacemacs/python-execute-file "execute file")
         ("C" spacemacs/python-execute-file-focus "execute file and focus"))
        ("d" "debug"
         (("b" :label "->b") spacemacs/python-toggle-breakpoint "toggle
                                                                 breakpoint"))
        ("r" "refactor"
         ("i" spacemacs/python-remove-unused-imports "remove unused import"))
        ("s" "REPL"
         ("s" spacemacs/python-shell-send-buffer-switch
          "send buffer to REPL and focus | on the buffer")
         ("S" python-shell-send-buffer
          ("send buffer to REPL" :label "buffer -> REPL | without
                                                          focusing"))
         ("d" spacemacs/python-shell-send-defun-switch
          "send function around point to REPL and focus")
         ("D" python-shell-send-defun
          "send function around point to REPL")
         ("r" spacemacs/python-shell-send-region-switch
          "send region to REPL and focus")
         ("R" python-shell-send-region "send region to REPL")))
       :minor
       (some-minor-mode
        ("a" "section under a key"
         ("b" foo-fn "call foo-fn")
         ("c" "sub section under c key"
          ("d" "sub sub section under d key"
           ("e" baz-fn "call baz-fn")))))
       (some-another-minor-mode
        ("a" "section under a key"
         ("b" "sub section under b key"
          ("c" "sub sub section under c key"
           ("b" baz-fn "call baz-fn")))))
       :global
       (("C-v" "section under a key"
         ("b" bar-fn "call bar-fn"))))
    (test-spacebind|validate-keys)
    (eq '())
    (should)))
