;;; test-flycheck-mode.el --- Flycheck Specs: Flycheck Mode  -*- lexical-binding: t; -*-

;; Copyright (C) 2013-2025 Sebastian Wiesner and Flycheck contributors

;; 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/>.

;;; Commentary:

;; Specs for Flycheck Mode and checker selection.

;;; Code:

(require 'flycheck-buttercup)
(require 'test-helpers)

(describe "Flycheck Mode"

  (describe "flycheck-mode"
    (it "enables-standard-error-navigation"
      (flycheck-buttercup-with-temp-buffer
        (setq next-error-function :old)
        (flycheck-mode 1)
        (expect flycheck-mode :to-be-truthy)
        (expect next-error-function :to-be 'flycheck-next-error-function)
        (flycheck-mode -1)
        (expect flycheck-mode :not :to-be-truthy)
        (expect next-error-function :to-be :old)))

    (it "does-not-enable-standard-error-navigation"
      (flycheck-buttercup-with-temp-buffer
        (let ((flycheck-standard-error-navigation nil))
          (setq next-error-function :old)
          (flycheck-mode +1)
          (expect flycheck-mode :to-be-truthy)
          (expect next-error-function :to-be :old)
          (expect flycheck-old-next-error-function :to-be :unset)
          (setq next-error-function :new)
          (flycheck-mode -1)
          (expect flycheck-mode :not :to-be-truthy)
          ;; Disabling the mode should not affect `next-error-function' now
          (expect next-error-function :to-be :new))))

    (it "clears-errors-after-revert"
      (flycheck-buttercup-with-resource-buffer "language/emacs-lisp/warnings.el"
        (emacs-lisp-mode)
        (goto-char (point-min))
        (insert "foo-bar")
        (flycheck-mode)
        (flycheck-buttercup-buffer-sync)
        (expect flycheck-current-errors :to-be-truthy)
        (let ((hack-local-variables-hook))
          (revert-buffer 'ignore-auto 'no-confirm))
        (expect flycheck-current-errors :not :to-be-truthy)
        (expect (flycheck-deferred-check-p) :not :to-be-truthy)))))

(describe "Syntax Checker Selection"

  (describe "flycheck-checker"
    (it "unusable-checker-causes-an-error"
      (flycheck-buttercup-with-temp-buffer
        (emacs-lisp-mode)
        (flycheck-mode)
        (let* ((flycheck-checker 'sh-bash))
          (flycheck-buffer)
          (expect flycheck-checker :to-be 'sh-bash)
          (expect flycheck-last-status-change :to-equal 'no-checker))))

    (it "usable-checker-is-used"
      (flycheck-buttercup-with-resource-buffer "language/emacs-lisp/warnings.el"
        (emacs-lisp-mode)
        (flycheck-mode)
        (expect (flycheck-get-checker-for-buffer) :to-be 'emacs-lisp)
        (let ((flycheck-checker 'emacs-lisp-checkdoc))
          (expect (flycheck-get-checker-for-buffer) :to-be 'emacs-lisp-checkdoc)
          (flycheck-buttercup-buffer-sync)
          (flycheck-buttercup-should-errors
           '(12 nil info "First sentence should end with punctuation"
                :checker emacs-lisp-checkdoc)))))

    (it "disabled-checker-is-not-used"
      (flycheck-buttercup-with-resource-buffer "language/emacs-lisp/warnings.el"
        (emacs-lisp-mode)
        (flycheck-mode)
        (let ((flycheck-disabled-checkers '(emacs-lisp emacs-lisp-checkdoc)))
          (expect (flycheck-get-checker-for-buffer) :not :to-be-truthy)
          (let* ((flycheck-checker 'emacs-lisp))
            (flycheck-buffer)
            (expect flycheck-checker :to-be 'emacs-lisp)
            (expect flycheck-last-status-change :to-equal 'no-checker)))))

    (it "unregistered-checker-is-used"
      (flycheck-buttercup-with-resource-buffer "language/emacs-lisp/warnings.el"
        (emacs-lisp-mode)
        (flycheck-mode)
        (expect (flycheck-get-checker-for-buffer) :to-be 'emacs-lisp)
        (let ((flycheck-checkers (remq 'emacs-lisp-checkdoc flycheck-checkers)))
          (expect (flycheck-registered-checker-p 'emacs-lisp-checkdoc)
                  :not :to-be-truthy)
          (let ((flycheck-checker 'emacs-lisp-checkdoc))
            (expect (flycheck-get-checker-for-buffer) :to-be 'emacs-lisp-checkdoc)
            (flycheck-buttercup-buffer-sync)
            (flycheck-buttercup-should-errors
             '(12 nil info "First sentence should end with punctuation"
                  :checker emacs-lisp-checkdoc)))))))

  (describe "flycheck-select-checker"
    (it "selecting-sets-the-syntax-checker"
      (flycheck-buttercup-with-temp-buffer
        (emacs-lisp-mode)
        (flycheck-select-checker 'emacs-lisp-checkdoc)
        (expect flycheck-checker :to-be 'emacs-lisp-checkdoc)))

    (it "unselecting-unsets-the-syntax-checker"
      (flycheck-buttercup-with-temp-buffer
        (emacs-lisp-mode)
        (flycheck-select-checker 'emacs-lisp-checkdoc)
        (flycheck-select-checker nil)
        (expect flycheck-checker :not :to-be-truthy)))

    (it "selecting-runs-a-syntax-check"
      (flycheck-buttercup-with-resource-buffer "language/emacs-lisp/warnings.el"
        (emacs-lisp-mode)
        (flycheck-mode)
        ;; By default we should get both, because emacs-lisp chains to checkdoc
        (flycheck-buttercup-buffer-sync)
        (dolist (err flycheck-current-errors)
          (expect (memq (flycheck-error-checker err)
                        '(emacs-lisp emacs-lisp-checkdoc))
                  :to-be-truthy))
        ;; We select checkdoc, and should now only have checkdoc errors
        (flycheck-select-checker 'emacs-lisp-checkdoc)
        (flycheck-buttercup-wait-for-syntax-checker)
        (dolist (err flycheck-current-errors)
          (expect (flycheck-error-checker err) :to-be 'emacs-lisp-checkdoc))))

    (it "unselecting-a-checker-goes-back-to-automatic-selection"
      (flycheck-buttercup-with-resource-buffer "language/emacs-lisp/warnings.el"
        (emacs-lisp-mode)
        (flycheck-mode)
        (flycheck-select-checker 'emacs-lisp-checkdoc)
        (expect flycheck-checker :to-be 'emacs-lisp-checkdoc)
        (flycheck-buttercup-wait-for-syntax-checker)
        (dolist (err flycheck-current-errors)
          (expect (flycheck-error-checker err) :to-be 'emacs-lisp-checkdoc))
        (flycheck-select-checker nil)
        (expect flycheck-checker :not :to-be-truthy)
        (flycheck-buttercup-wait-for-syntax-checker)
        (dolist (err flycheck-current-errors)
          (expect (memq (flycheck-error-checker err)
                        '(emacs-lisp emacs-lisp-checkdoc))
                  :to-be-truthy)))))

  (describe "flycheck/selects-checker-automatically"
    (it "no-disabled-checker"
      (flycheck-buttercup-with-resource-buffer "language/emacs-lisp/warnings.el"
        (let ((flycheck-disabled-checkers '(emacs-lisp)))
          (flycheck-buttercup-buffer-sync)
          (expect flycheck-checker :not :to-be-truthy)
          (flycheck-buttercup-should-errors
           '(12 nil info "First sentence should end with punctuation"
                :checker emacs-lisp-checkdoc))))))

  (describe "flycheck-disable-checker"
    (it "disables-checker"
      (flycheck-buttercup-with-temp-buffer
        (flycheck-mode)
        (with-no-warnings
          (flycheck-disable-checker 'emacs-lisp))
        (expect flycheck-disabled-checkers :to-equal '(emacs-lisp))
        (expect (default-value 'flycheck-disabled-checkers) :not :to-be-truthy)
        ;; Disabling a disabled checker should be a no-op
        (with-no-warnings
          (flycheck-disable-checker 'emacs-lisp))
        (expect flycheck-disabled-checkers :to-equal '(emacs-lisp))))

    (it "enables-checker"
      (flycheck-buttercup-with-temp-buffer
        (flycheck-mode)
        (setq flycheck-disabled-checkers '(emacs-lisp python-pylint))
        (with-no-warnings
          (flycheck-disable-checker 'emacs-lisp 'enable))
        (expect flycheck-disabled-checkers :to-equal '(python-pylint))))))

(describe "Reporting that nothing can check a buffer"
  ;; Without this the mode line just sits at "FlyC-" and someone who has
  ;; only just enabled Flycheck has no idea anything is wrong
  (defun test-nochecker/messages (mode &optional usable)
    "Return the messages MODE produces over repeated checks."
    (let ((buffer (generate-new-buffer "flycheck-test-nochecker"))
          (messages nil))
      (unwind-protect
          (with-current-buffer buffer
            (funcall mode)
            (setq-local flycheck-check-syntax-automatically nil)
            (cl-letf (((symbol-function 'message)
                       (lambda (fmt &rest args)
                         (when fmt (push (apply #'format fmt args) messages))))
                      ((symbol-function 'flycheck-may-use-checker)
                       (lambda (&rest _) usable)))
              (flycheck-mode 1)
              (dotimes (_ 3) (ignore-errors (flycheck-buffer))))
            (nreverse messages))
        (kill-buffer buffer))))

  (before-each (setq flycheck--modes-without-checker nil))
  (after-all (setq flycheck--modes-without-checker nil))

  (it "says so when no checker supports the mode at all"
    (let ((messages (test-nochecker/messages #'fundamental-mode)))
      (expect (car messages) :to-match "no syntax checker supports fundamental-mode")))

  (it "says something different when checkers exist but none can run"
    ;; That usually means the tool is missing, which is worth chasing
    (let ((messages (test-nochecker/messages #'emacs-lisp-mode)))
      (expect (car messages) :to-match "can run here")))

  (it "says it once, however many times the buffer is checked"
    (expect (length (test-nochecker/messages #'fundamental-mode)) :to-equal 1))

  (it "stays quiet for a mode it already mentioned"
    (test-nochecker/messages #'fundamental-mode)
    (expect (test-nochecker/messages #'fundamental-mode) :to-be nil))

  (it "keeps out of ephemeral buffers"
    (flycheck-buttercup-with-temp-buffer
      (fundamental-mode)
      (let ((said nil))
        (cl-letf (((symbol-function 'message) (lambda (&rest _) (setq said t))))
          (flycheck--report-no-checker))
        (expect said :to-be nil)))))

(describe "Setup verification"

  (describe "flycheck--verify-princ-last-failure"
    (defun test-verify/render (failure)
      "Return what FAILURE renders as in the verification buffer."
      (with-temp-buffer
        (let ((standard-output (current-buffer)))
          (flycheck--verify-princ-last-failure failure))
        (buffer-string)))

    (it "shows nothing when the last check did not fail"
      (expect (test-verify/render nil) :to-equal ""))

    (it "shows a checker's own output when Flycheck could not read it"
      ;; The echo area only gets a short message, so the crash dump that
      ;; explains the problem has to be reachable from here
      (let ((rendered (test-verify/render
                       '(python-flake8 suspicious
                                       "ModuleNotFoundError: pycodestyle"))))
        (expect rendered :to-match "could not read python-flake8's output")
        (expect rendered :to-match "ModuleNotFoundError: pycodestyle")
        (expect rendered :to-match "misconfigured")))

    (it "distinguishes a checker that could not be run at all"
      (expect (test-verify/render '(python-flake8 errored "no such file"))
              :to-match "could not run python-flake8"))

    (it "copes with a failure that carried no output"
      (expect (test-verify/render '(python-flake8 suspicious nil))
              :to-match "nothing at all"))))

;;; test-flycheck-mode.el ends here
