;;; modus-themes-test.el --- Unit tests for the Modus themes -*- lexical-binding: t -*-

;; Copyright (C) 2025-2026  Free Software Foundation, Inc.

;; Author: Protesilaos <info@protesilaos.com>,
;;         Rudolf Adamkovič <rudolf@adamkovic.org>
;; Maintainer: Protesilaos <info@protesilaos.com>
;; URL: https://github.com/protesilaos/modus-themes

;; 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 <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Tests for the Modus themes.  Note that we are using Shorthands in
;; this file, so the "mtt-" prefix really is "modus-themes-test-".
;; Evaluate the following to learn more:
;;
;;    (info "(elisp) Shorthands")

;;; Code:

(require 'ert)
(require 'modus-themes)

(defmacro mtt-define-test (symbol &rest body)
  "Write test for SYMBOL with BODY forms.
If the `car' of BODY is a string, use it as the docstring of the test.
Else provide a generic snippet of text."
  (declare (doc-string 2) (indent defun))
  (let* ((first (car body))
         (has-docstring (stringp first))
         (docstring (if has-docstring
                        first
                      (format "Test that `%s' does the right thing." symbol)))
         (body (if has-docstring (cdr body) body)))
    `(ert-deftest ,(intern (format "mtt-%s" symbol)) ()
       ,docstring
       ,@body)))

(mtt-define-test modus-themes--color-hex-p
  (should (modus-themes--color-hex-p "#123"))
  (should (modus-themes--color-hex-p "#123456"))
  (should (modus-themes--color-hex-p "#abc"))
  (should (modus-themes--color-hex-p "#ABCDEF"))
  (should-not (modus-themes--color-hex-p "abc"))
  (should-not (modus-themes--color-hex-p "#abcd"))
  (should-not (modus-themes--color-hex-p "#12345"))
  (should-not (modus-themes--color-hex-p "#1234567"))
  (should-not (modus-themes--color-hex-p "#zzz"))
  (should-not (modus-themes--color-hex-p "#zzz"))
  (should-not (modus-themes--color-hex-p "#12g456")))

(mtt-define-test modus-themes--hex-or-name-to-rgb
  (should (equal (modus-themes--hex-or-name-to-rgb "#fff") (list 1.0 1.0 1.0)))
  (should (equal (modus-themes--hex-or-name-to-rgb "#000") (list 0.0 0.0 0.0)))
  (should (equal (modus-themes--hex-or-name-to-rgb "#f00") (list 1.0 0.0 0.0)))
  (should (equal (modus-themes--hex-or-name-to-rgb "#0f0") (list 0.0 1.0 0.0)))
  (should (equal (modus-themes--hex-or-name-to-rgb "#00f") (list 0.0 0.0 1.0)))
  (should (equal (modus-themes--hex-or-name-to-rgb "#ffffff") (list 1.0 1.0 1.0)))
  (should (equal (modus-themes--hex-or-name-to-rgb "#000000") (list 0.0 0.0 0.0)))
  (should (equal (modus-themes--hex-or-name-to-rgb "#ff0000") (list 1.0 0.0 0.0)))
  (should (equal (modus-themes--hex-or-name-to-rgb "#00ff00") (list 0.0 1.0 0.0)))
  ;; NOTE 2026-04-12: I do not need it to be precise here, hence the `format'.
  (let ((rgb-rounded-fn
         (lambda (hex)
           (let ((rgb (modus-themes--hex-or-name-to-rgb hex)))
             (mapcar (lambda (float) (string-to-number (format "%.2f" float))) rgb)))))
    (should (equal (funcall rgb-rounded-fn "#800000") (list 0.5 0.0 0.0)))
    (should (equal (funcall rgb-rounded-fn "#008000") (list 0.0 0.5 0.0)))
    (should (equal (funcall rgb-rounded-fn "#000080") (list 0.0 0.0 0.5))))
  (should-error (modus-themes--hex-or-name-to-rgb ""))
  (should-error (modus-themes--hex-or-name-to-rgb "#"))
  (should-error (modus-themes--hex-or-name-to-rgb "#1"))
  (should-error (modus-themes--hex-or-name-to-rgb "#12"))
  (should-error (modus-themes--hex-or-name-to-rgb "#1234"))
  (should-error (modus-themes--hex-or-name-to-rgb "#12345"))
  (should-error (modus-themes--hex-or-name-to-rgb "#gggggg"))
  (should-error (modus-themes--hex-or-name-to-rgb (list 1.0 1.0 1.0))))

(mtt-define-test modus-themes-wcag-formula
  "Test that `modus-themes-wcag-formula' does the right thing.
Also see `modus-themes-test--modus-themes--hex-to-rgb'."
  (should (= (modus-themes-wcag-formula "#ffffff") 1.0))
  (should (= (modus-themes-wcag-formula "#000000") 0.0))
  (should-error (modus-themes-wcag-formula "#00000")))

(mtt-define-test modus-themes-contrast
  "Test that `modus-themes-contrast' works as intended.
Also see `modus-themes-test--modus-themes--hex-to-rgb'."
  (should (= (modus-themes-contrast "#ffffff" "#000000") 21.0))
  (should (= (modus-themes-contrast "#000000" "#ffffff") 21.0))
  ;; NOTE 2026-04-12: I do not need it to be precise here, hence the `format'.
  (let ((float-2-fn (lambda (hex1 hex2) (string-to-number (format "%.2f" (modus-themes-contrast hex1 hex2))))))
    (should (= (funcall float-2-fn "#ff0000" "#ffffff")  4.0))
    (should (= (funcall float-2-fn "#00ff00" "#ffffff") 1.37))
    (should (= (funcall float-2-fn "#0000ff" "#ffffff") 8.59))
    (should (= (funcall float-2-fn "#ffff00" "#ffffff") 1.07))
    (should (= (funcall float-2-fn "#00ffff" "#ffffff") 1.25))
    (should (= (funcall float-2-fn "#ff00ff" "#ffffff") 3.14)))
  (should-error (modus-themes-contrast "#ffffff" "#00000"))
  (should-error (modus-themes-contrast "#fffff" "#00000")))

(mtt-define-test modus-themes--color-eight-to-six-digits
  (should (string= (modus-themes--color-eight-to-six-digits "#f00") "#f00"))
  (should (string= (modus-themes--color-eight-to-six-digits "#ff1919") "#ff1919"))
  (should (string= (modus-themes--color-eight-to-six-digits "#ffff19991999") "#ff1919")))

(mtt-define-test modus-themes-adjust-value
  "Test that `modus-themes-adjust-value' does the right thing.
Also see `modus-themes-test--modus-themes--hex-to-rgb'."
  (should (string= (modus-themes-adjust-value "#ff0000" 10) "#ff1919"))
  (should (string= (modus-themes-adjust-value "#505050" 100) "#ffffff"))
  (should (string= (modus-themes-adjust-value "#505050" -100) "#000000"))
  (should (string= (modus-themes-adjust-value "#505050" 0) "#505050"))
  (should-error (modus-themes-adjust-value "#ff00" 10)))

(mtt-define-test modus-themes-activate
  (if (custom-theme-p 'modus-operandi-tritanopia)
      (should-not (modus-themes-activate 'modus-operandi-tritanopia))
    (should (custom-theme-p 'modus-operandi-tritanopia))))

(mtt-define-test modus-themes--belongs-to-family-p
  (should (modus-themes--belongs-to-family-p 'modus-operandi 'modus-themes))
  (should-not (modus-themes--belongs-to-family-p 'my-fancy-theme 'modus-themes))
  (should-not (modus-themes--belongs-to-family-p 'modus-operandi 'my-fancy-themes)))

(mtt-define-test modus-themes-get-all-known-themes
  (should (equal (modus-themes-get-all-known-themes) modus-themes-items))
  (should-not (modus-themes-get-all-known-themes 'my-fancy-themes)))

(mtt-define-test modus-themes--background-p
  (should (modus-themes--background-p 'modus-operandi 'light))
  (should-not (modus-themes--background-p 'modus-operandi 'dark))
  (should-not (modus-themes--background-p 'modus-operandi t))
  (should-not (modus-themes--background-p 'modus-operandi :light)))

(mtt-define-test modus-themes-sort
  (let ((first-has-prefix-fn (lambda (themes prefix)
                               (when-let* ((first (car themes))
                                           (name (symbol-name first)))
                                 (string-match-p prefix name)))))
    (should (funcall first-has-prefix-fn (modus-themes-sort (reverse modus-themes-items) 'light) "modus-operandi"))
    (should (funcall first-has-prefix-fn (modus-themes-sort modus-themes-items 'dark) "modus-vivendi"))))

(mtt-define-test inheritance
  "Ensure all faces inherit from valid faces."
  ;; Third-party packages, loaded if possible to better test face inheritance.
  (require 'font-latex nil t)
  (let ((current-theme (modus-themes-get-current-theme)))
    (unwind-protect
        (progn
          (modus-themes-load-theme 'modus-operandi)
          (should-not (seq-filter
                       (lambda (face)
                         ;; The face either has no parent ...
                         (if-let* ((parent (face-attribute face :inherit)))
                             (and (symbolp parent)
                                  (not (eq parent 'unspecified))
                                  ;; ... or its parent is a valid face.
                                  (not (facep parent)))))
                       (face-list))))
      (modus-themes-load-theme current-theme))))

(mtt-define-test color-dark-p
  (let ((modus-operandi-sample-foregrounds
         '("#a60000"
           "#972500"
           "#a0132f"
           "#7f0000"
           "#006800"
           "#316500"
           "#00663f"
           "#2a5045"
           "#6f5500"
           "#884900"
           "#7a4f2f"
           "#624416"
           "#0031a9"
           "#3548cf"
           "#0000b0"
           "#003497"
           "#721045"
           "#8f0075"
           "#531ab6"
           "#7c318f"
           "#005e8b"
           "#3f578f"
           "#005f5f"
           "#005077"))
        (modus-vivendi-sample-foregrounds
         '("#ff5f59"
           "#ff6b55"
           "#ff7f86"
           "#ff9580"
           "#44bc44"
           "#70b900"
           "#00c06f"
           "#88ca9f"
           "#d0bc00"
           "#fec43f"
           "#dfaf7a"
           "#d2b580"
           "#2fafff"
           "#79a8ff"
           "#00bcff"
           "#82b0ec"
           "#feacd0"
           "#f78fe7"
           "#b6a0ff"
           "#caa6df"
           "#00d3d0"
           "#4ae2f0"
           "#6ae4b9"
           "#9ac8e0")))
    (should (seq-every-p #'modus-themes-color-dark-p modus-operandi-sample-foregrounds))
    (should-not (seq-every-p #'modus-themes-color-dark-p modus-vivendi-sample-foregrounds))))

(mtt-define-test get-readable-foreground
  (let ((modus-operandi-sample-foregrounds
         '("#a60000"
           "#972500"
           "#a0132f"
           "#7f0000"
           "#006800"
           "#316500"
           "#00663f"
           "#2a5045"
           "#6f5500"
           "#884900"
           "#7a4f2f"
           "#624416"
           "#0031a9"
           "#3548cf"
           "#0000b0"
           "#003497"
           "#721045"
           "#8f0075"
           "#531ab6"
           "#7c318f"
           "#005e8b"
           "#3f578f"
           "#005f5f"
           "#005077"))
        (modus-vivendi-sample-foregrounds
         '("#ff5f59"
           "#ff6b55"
           "#ff7f86"
           "#ff9580"
           "#44bc44"
           "#70b900"
           "#00c06f"
           "#88ca9f"
           "#d0bc00"
           "#fec43f"
           "#dfaf7a"
           "#d2b580"
           "#2fafff"
           "#79a8ff"
           "#00bcff"
           "#82b0ec"
           "#feacd0"
           "#f78fe7"
           "#b6a0ff"
           "#caa6df"
           "#00d3d0"
           "#4ae2f0"
           "#6ae4b9"
           "#9ac8e0")))
    (should (seq-every-p
             (lambda (value)
               (string= value "#ffffff"))
             (mapcar #'modus-themes-get-readable-foreground modus-operandi-sample-foregrounds)))
    (should (seq-every-p
             (lambda (value)
               (string= value "#000000"))
             (mapcar #'modus-themes-get-readable-foreground modus-vivendi-sample-foregrounds)))))

(mtt-define-test complete-palette-mappings
  "Ensure all built-in palettes contain entries for all palette mappings.
This computes a complete list of mapping names from the built-in palettes
and then cross-checks the palettes against the list. This should help ensure
consistency and completeness where adding a new mapping in one palette will
fail this test until it is added in all palettes."
  (let* ((operandi-names (seq-uniq (mapcar #'car modus-themes-operandi-palette)))
         (operandi-tinted-names (seq-uniq (mapcar #'car modus-themes-operandi-tinted-palette)))
         (operandi-deuteranopia-names (seq-uniq (mapcar #'car modus-themes-operandi-deuteranopia-palette)))
         (operandi-tritanopia-names (seq-uniq (mapcar #'car modus-themes-operandi-tritanopia-palette)))
         (vivendi-names (seq-uniq (mapcar #'car modus-themes-vivendi-palette)))
         (vivendi-tinted-names (seq-uniq (mapcar #'car modus-themes-vivendi-tinted-palette)))
         (vivendi-deuteranopia-names (seq-uniq (mapcar #'car modus-themes-vivendi-deuteranopia-palette)))
         (vivendi-tritanopia-names (seq-uniq (mapcar #'car modus-themes-vivendi-tritanopia-palette)))
         (all-names (seq-uniq (append operandi-names
                                      operandi-tinted-names
                                      operandi-deuteranopia-names
                                      operandi-tritanopia-names
                                      vivendi-names
                                      vivendi-tinted-names
                                      vivendi-deuteranopia-names
                                      vivendi-tritanopia-names))))
    (should (seq-empty-p (seq-difference all-names operandi-names)))
    (should (seq-empty-p (seq-difference all-names operandi-tinted-names)))
    (should (seq-empty-p (seq-difference all-names operandi-deuteranopia-names)))
    (should (seq-empty-p (seq-difference all-names operandi-tritanopia-names)))
    (should (seq-empty-p (seq-difference all-names vivendi-names)))
    (should (seq-empty-p (seq-difference all-names vivendi-tinted-names)))
    (should (seq-empty-p (seq-difference all-names vivendi-deuteranopia-names)))
    (should (seq-empty-p (seq-difference all-names vivendi-tritanopia-names)))))

(mtt-define-test modus-themes-generate-palette
  "Ensure `modus-themes-generate-palette' provides enough mapping coverage
such that the generated palette does not inherit explicit color values
from a parent core palette."
  (let* ((generated-palette (modus-themes-generate-palette
                             '((fg-main "#ffffff")
                               (bg-main "#000000")
                               (red "#ff0000")
                               (green "#00ff00")
                               (yellow "#ffff00")
                               (blue "#0000ff")
                               (magenta "#ff00ff")
                               (cyan "#00ffff"))))
         ;; These are colors we expect to have an explicit hex value,
         ;; either because they were provided explicitly in the call to
         ;; `modus-themes-generate-palette' above, or because we expect
         ;; `modus-themes-generate-palette' to generate the color.
         (explicit-colors '( fg-main bg-main
                             fg-alt fg-dim bg-inactive bg-active bg-dim border
                             ;; cyan
                             cyan bg-cyan-nuanced bg-cyan-subtle bg-cyan-intense
                             cyan-intense cyan-faint cyan-cooler cyan-warmer
                             ;; magenta
                             magenta bg-magenta-nuanced bg-magenta-subtle bg-magenta-intense
                             magenta-intense magenta-faint magenta-cooler magenta-warmer
                             ;; blue
                             blue bg-blue-nuanced bg-blue-subtle bg-blue-intense
                             blue-intense blue-faint blue-cooler blue-warmer
                             ;; yellow
                             yellow bg-yellow-nuanced bg-yellow-subtle bg-yellow-intense
                             yellow-intense yellow-faint yellow-cooler yellow-warmer
                             ;; green
                             green bg-green-nuanced bg-green-subtle bg-green-intense
                             green-intense green-faint green-cooler green-warmer
                             ;; red
                             red bg-red-nuanced bg-red-subtle bg-red-intense
                             red-intense red-faint red-cooler red-warmer)))
    ;; Ensure all other colors in the palette map to a color name and not an
    ;; explicit hex value. If this check fails, the offending color should
    ;; be mapped to an appropriate color name in `modus-themes-generate-palette'.
    (should (seq-empty-p (seq-remove (lambda (elem)
                                       (or (member (car elem) explicit-colors)
                                           (symbolp (cadr elem))))
                                     generated-palette)))))

(provide 'modus-themes-test)
;;; modus-themes-test.el ends here

;; Local Variables:
;; read-symbol-shorthands: (("mtt" . "modus-themes-test-"))
;; End:
