;;; test-config.el --- ERT tests for config.el  -*- lexical-binding: t; -*-

(require 'ert)
(require 'config)
(require 'vc-git)

(defvar test-config--visited-files nil
  "List of files visited via stubbed `find-file' during tests.")

(defun test-config--mock-find-file (filename &rest _)
  "Stub `find-file': record FILENAME, create buffer with contents, and switch to it."
  (push filename test-config--visited-files)
  (let ((buf (generate-new-buffer " *test-visit*")))
    (when (file-readable-p filename)
      (with-current-buffer buf
        (insert-file-contents filename)))
    (switch-to-buffer buf)
    buf))

(defmacro test-config--with-temp-dir (&rest body)
  "Create a temporary directory, bind `user-emacs-directory', execute BODY, then cleanup."
  (declare (indent 0))
  `(let* ((tmp-dir (make-temp-file "config.el-" t))
          (user-emacs-directory (file-name-as-directory tmp-dir)))
     (unwind-protect
         (progn ,@body)
       (when (file-directory-p tmp-dir)
         (delete-directory tmp-dir t)))))

;;; Internal helpers (config--locate-init-file, config--in-comment-p,
;;; config--scan-use-packages, config--config-directory,
;;; config--file-in-excluded-dir-p, config--collect-packages)

(ert-deftest test-config--locate-init-file-finds-init-el ()
  "Should find init.el in user-emacs-directory."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory)))
      (write-region "" nil init-file)
      (should (equal (config--locate-init-file) init-file)))))

(ert-deftest test-config--locate-init-file-returns-nil-when-none-exist ()
  "Should return nil when no init file exists."
  (test-config--with-temp-dir
    (should-not (config--locate-init-file))))

(ert-deftest test-config--locate-init-file-prefers-init-el ()
  "Should prefer init.el over .emacs when both exist."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (dot-emacs (expand-file-name ".emacs" user-emacs-directory)))
      (write-region "(init)" nil init-file)
      (write-region "(dot-emacs)" nil dot-emacs)
      (should (equal (config--locate-init-file) init-file)))))

(ert-deftest test-config--scan-use-packages-finds-declarations ()
  "Should find use-package declarations in a file."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "test.el" user-emacs-directory)))
      (write-region "(use-package magit)\n(use-package org)\n" nil test-file)
      (let ((packages (config--scan-use-packages test-file)))
        (should (= (length packages) 2))
        (should (equal (nth 0 (nth 0 packages)) 'magit))
        (should (equal (nth 0 (nth 1 packages)) 'org))))))

(ert-deftest test-config--scan-use-packages-ignores-comments ()
  "Should ignore use-package forms inside comments."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "test.el" user-emacs-directory)))
      (write-region ";; (use-package disabled)\n(use-package enabled)\n" nil test-file)
      (let ((packages (config--scan-use-packages test-file)))
        (should (= (length packages) 1))
        (should (equal (nth 0 (nth 0 packages)) 'enabled))))))

(ert-deftest test-config--scan-use-packages-hyphenated-names ()
  "Should find use-package declarations with hyphenated package names."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "test.el" user-emacs-directory)))
      (write-region "(use-package ivy-hydra)\n(use-package which-key)\n" nil test-file)
      (let ((packages (config--scan-use-packages test-file)))
        (should (= (length packages) 2))
        (should (equal (nth 0 (nth 0 packages)) 'ivy-hydra))
        (should (equal (nth 0 (nth 1 packages)) 'which-key))))))

(ert-deftest test-config--scan-use-packages-line-numbers ()
  "Should report correct line numbers for use-package declarations."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "test.el" user-emacs-directory)))
      (write-region ";; header\n\n(use-package magit)\n" nil test-file)
      (let ((packages (config--scan-use-packages test-file)))
        (should (= (length packages) 1))
        (should (= (nth 2 (nth 0 packages)) 3))))))

(ert-deftest test-config--scan-use-packages-empty-file ()
  "Should return nil for a file with no use-package declarations."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "empty.el" user-emacs-directory)))
      (write-region "" nil test-file)
      (should-not (config--scan-use-packages test-file)))))

(ert-deftest test-config--config-directory-with-user-emacs-dir ()
  "Should return user-emacs-directory when it exists."
  (test-config--with-temp-dir
    (let ((result (config--config-directory)))
      (should (equal result (file-name-as-directory user-emacs-directory))))))

(ert-deftest test-config--file-in-excluded-dir-p-excludes-elpa ()
  "Should identify files inside elpa as excluded."
  (test-config--with-temp-dir
    (let ((dir user-emacs-directory))
      (should (config--file-in-excluded-dir-p
               (expand-file-name "elpa/pkg/pkg.el" dir) dir)))))

(ert-deftest test-config--file-in-excluded-dir-p-excludes-straight ()
  "Should identify files inside straight as excluded."
  (test-config--with-temp-dir
    (let ((dir user-emacs-directory))
      (should (config--file-in-excluded-dir-p
               (expand-file-name "straight/pkg/pkg.el" dir) dir)))))

(ert-deftest test-config--file-in-excluded-dir-p-allows-normal-files ()
  "Should not exclude files in normal directories."
  (test-config--with-temp-dir
    (let ((dir user-emacs-directory))
      (should-not (config--file-in-excluded-dir-p
                   (expand-file-name "lisp/my-pkg.el" dir) dir)))))

(ert-deftest test-config--file-in-excluded-dir-p-custom-exclude-list ()
  "Should respect custom `config-scan-exclude-directories'."
  (test-config--with-temp-dir
    (let ((dir user-emacs-directory)
          (config-scan-exclude-directories '("custom-exclude")))
      (should (config--file-in-excluded-dir-p
               (expand-file-name "custom-exclude/pkg.el" dir) dir))
      (should-not (config--file-in-excluded-dir-p
                   (expand-file-name "elpa/pkg.el" dir) dir)))))

(ert-deftest test-config--collect-packages-finds-packages ()
  "Should collect use-package declarations from .el files."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "init.el" user-emacs-directory)))
      (write-region "(use-package magit)\n(use-package org)\n" nil test-file)
      (let ((packages (config--collect-packages)))
        (should (>= (length packages) 2))
        (should (cl-find-if (lambda (p) (equal (nth 0 p) 'magit)) packages))
        (should (cl-find-if (lambda (p) (equal (nth 0 p) 'org)) packages))))))

(ert-deftest test-config--collect-packages-excludes-elpa ()
  "Should not collect packages from elpa directory."
  (test-config--with-temp-dir
    (let* ((dir user-emacs-directory)
           (elpa-dir (expand-file-name "elpa/pkg-1.0/" dir))
           (init-file (expand-file-name "init.el" dir)))
      (make-directory elpa-dir t)
      (write-region "(use-package magit)\n" nil init-file)
      (write-region "(use-package elpa-pkg)\n" nil (expand-file-name "pkg.el" elpa-dir))
      (let ((packages (config--collect-packages)))
        (should-not (cl-find-if (lambda (p) (equal (nth 0 p) 'elpa-pkg)) packages))))))

(ert-deftest test-config--collect-packages-no-config-dir ()
  "Should signal a user-error when no config directory is found."
  (let ((user-emacs-directory "/nonexistent/path/"))
    (should-error (config--collect-packages) :type 'user-error)))

(ert-deftest test-config--in-comment-p-detects-semicolon ()
  "Should detect when match is preceded by a semicolon."
  (with-temp-buffer
    (insert ";; (use-package foo)\n")
    (goto-char (point-min))
    (re-search-forward "(use-package" nil t)
    (should (config--in-comment-p))))

(ert-deftest test-config--in-comment-p-no-semicolon ()
  "Should return nil when match is not preceded by a semicolon."
  (with-temp-buffer
    (insert "(use-package foo)\n")
    (goto-char (point-min))
    (re-search-forward "(use-package" nil t)
    (should-not (config--in-comment-p))))

(ert-deftest test-config--scan-use-packages-ignores-inline-comment ()
  "Should ignore use-package when a semicolon precedes it on the same line."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "test.el" user-emacs-directory)))
      (write-region "foo ; (use-package disabled)\n(use-package enabled)\n" nil test-file)
      (let ((packages (config--scan-use-packages test-file)))
        (should (= (length packages) 1))
        (should (equal (nth 0 (nth 0 packages)) 'enabled))))))

(ert-deftest test-config--scan-use-packages-nested-forms ()
  "Should find use-package inside other forms like `when'."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "test.el" user-emacs-directory)))
      (write-region "(when (featurep 'x)\n  (use-package magit))\n" nil test-file)
      (let ((packages (config--scan-use-packages test-file)))
        (should (= (length packages) 1))
        (should (equal (nth 0 (nth 0 packages)) 'magit))))))

;;; M-x config-visit

(ert-deftest test-config-visit-opens-existing-init ()
  "config-visit should open the existing init.el file."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (test-config--visited-files nil))
      (write-region "(setq x 1)\n" nil init-file)
      (cl-letf (((symbol-function 'find-file) #'test-config--mock-find-file))
        (config-visit)
        (should (= (length test-config--visited-files) 1))
        (should (equal (car test-config--visited-files) init-file))))))

(ert-deftest test-config-visit-creates-init-when-absent ()
  "config-visit should create init.el when none exists and user confirms."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (test-config--visited-files nil))
      (cl-letf (((symbol-function 'find-file) #'test-config--mock-find-file)
                ((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (config-visit)
        (should (file-exists-p init-file))
        (should (= (length test-config--visited-files) 1))
        (should (equal (car test-config--visited-files) init-file))))))

(ert-deftest test-config-visit-declines-create ()
  "config-visit should signal user-error when user declines to create init.el."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (test-config--visited-files nil))
      (cl-letf (((symbol-function 'find-file) #'test-config--mock-find-file)
                ((symbol-function 'y-or-n-p) (lambda (_prompt) nil)))
        (should-error (config-visit) :type 'user-error)
        (should-not (file-exists-p init-file))))))

;;; M-x config-visit-package

(ert-deftest test-config-visit-package-opens-file ()
  "config-visit-package should open the file containing the use-package form."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (test-config--visited-files nil))
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'find-file) #'test-config--mock-find-file))
        (config-visit-package "magit")
        (should (= (length test-config--visited-files) 1))
        (should (equal (car test-config--visited-files) init-file))))))

(ert-deftest test-config-visit-package-errors-on-missing ()
  "config-visit-package should signal user-error for unknown package name."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory)))
      (write-region "(use-package magit)\n" nil init-file)
      (should-error (config-visit-package "nonexistent-pkg") :type 'user-error))))

(ert-deftest test-config-visit-package-jumps-to-line ()
  "config-visit-package should position point at the correct line."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (test-config--visited-files nil)
          visited-buffer)
      (write-region ";; comment\n\n(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'find-file)
                 (lambda (filename &rest _)
                   (push filename test-config--visited-files)
                   (setq visited-buffer (generate-new-buffer " *test-visit*"))
                   (with-current-buffer visited-buffer
                     (insert-file-contents filename))
                   (switch-to-buffer visited-buffer)
                   visited-buffer)))
        (config-visit-package "magit")
        (should (= (length test-config--visited-files) 1))
        (with-current-buffer visited-buffer
          (should (= (line-number-at-pos) 3)))))))

;;; M-x config-install-package

(ert-deftest test-config--uninstalled-packages-filters-installed ()
  "Should return only packages not detected as installed."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory)))
      (write-region "(use-package magit)\n(use-package nonexistent-pkg)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p)
                 (lambda (name) (eq name 'magit))))
        (let ((names (config--uninstalled-packages)))
          (should (member "nonexistent-pkg" names))
          (should-not (member "magit" names)))))))

(ert-deftest test-config--uninstalled-packages-empty-when-all-installed ()
  "Should return nil when all declared packages are installed."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory)))
      (write-region "(use-package magit)\n(use-package org)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) t)))
        (should-not (config--uninstalled-packages))))))

(ert-deftest test-config--uninstalled-packages-all-uninstalled ()
  "Should return all packages when none are installed."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory)))
      (write-region "(use-package magit)\n(use-package org)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) nil)))
        (let ((names (config--uninstalled-packages)))
          (should (member "magit" names))
          (should (member "org" names)))))))

(ert-deftest test-config-install-package-calls-package-install ()
  "Should call package-install with the selected package symbol."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (installed-pkg nil))
      (write-region "(use-package nonexistent-pkg)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) nil))
                ((symbol-function 'package-install)
                 (lambda (pkg) (setq installed-pkg pkg)))
                ((symbol-function 'package-installed-p) (lambda (_name) nil))
                ((symbol-function 'package-refresh-contents) (lambda ())))
        ;; Provide a non-nil package-archive-contents so the function
        ;; skips the refresh step.  Must use dynamic binding since
        ;; package-archive-contents is defined with defvar in package.el.
        (defvar package-archive-contents)
        (let ((package-archive-contents '((nonexistent-pkg . [1 0]))))
          (config-install-package "nonexistent-pkg")
          (should (eq installed-pkg 'nonexistent-pkg)))))))

(ert-deftest test-config-install-package-errors-when-all-installed ()
  "Should signal user-error when all declared packages are already installed."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory)))
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) t)))
        (should-error
         (call-interactively 'config-install-package)
         :type 'user-error)))))

(ert-deftest test-config-install-all-packages-installs-missing ()
  "Should install all uninstalled packages declared in config."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (installed nil))
      (write-region "(use-package magit)\n(use-package org)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p)
                 (lambda (name) (eq name 'magit)))
                ((symbol-function 'package-install)
                 (lambda (pkg) (push pkg installed)))
                ((symbol-function 'package-installed-p)
                 (lambda (name) (eq name 'magit)))
                ((symbol-function 'package-refresh-contents) (lambda ()))
                ((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (defvar package-archive-contents)
        (let ((package-archive-contents '((org . [1 0]))))
          (config-install-all-packages)
          (should (member 'org installed))
          (should-not (member 'magit installed)))))))

(ert-deftest test-config-install-all-packages-skips-when-all-installed ()
  "Should message when all declared packages are already installed."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (msg nil))
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) t))
                ((symbol-function 'message) (lambda (fmt &rest args) (setq msg (apply #'format fmt args)))))
        (config-install-all-packages)
        (should (string-match-p "already installed" msg))))))

;;; M-x config-delete-package

(ert-deftest test-config-delete-package-removes-installed ()
  "Should delete an installed declared package."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (deleted-pkg nil)
          (mock-desc (make-vector 5 nil)))
      (aset mock-desc 0 'magit)
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) t))
                ((symbol-function 'package-delete)
                 (lambda (desc) (setq deleted-pkg (aref desc 0))))
                ((symbol-function 'package-installed-p) (lambda (_name) t)))
        (defvar package-alist)
        (let ((package-alist (list (list 'magit mock-desc))))
          (config-delete-package "magit")
          (should (eq deleted-pkg 'magit)))))))

(ert-deftest test-config-delete-package-errors-when-not-installed ()
  "Should signal user-error when package is not installed."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory)))
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) nil)))
        (should-error (config-delete-package "magit") :type 'user-error)))))

(ert-deftest test-config-delete-package-errors-when-no-installed ()
  "Should signal user-error when no declared packages are installed."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory)))
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) nil)))
        (should-error
         (call-interactively 'config-delete-package)
         :type 'user-error)))))

;;; M-x config-delete-all-packages

(ert-deftest test-config-delete-all-packages-deletes-installed ()
  "Should delete all installed declared packages."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (delete-count 0))
      (write-region "(use-package magit)\n(use-package org)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) t))
                ((symbol-function 'package-delete)
                 (lambda (_desc) (setq delete-count (1+ delete-count)))))
        (defvar package-alist)
        (let ((package-alist '((magit [pkg-desc]) (org [pkg-desc]))))
          (config-delete-all-packages)
          (should (= delete-count 2)))))))

(ert-deftest test-config-delete-all-packages-skips-when-none-installed ()
  "Should message when no declared packages are installed."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (msg nil))
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) nil))
                ((symbol-function 'message) (lambda (fmt &rest args) (setq msg (apply #'format fmt args)))))
        (config-delete-all-packages)
        (should (string-match-p "No installed" msg))))))

(ert-deftest test-config-packages-install-calls-install ()
  "config-packages-install should call config-install-package for the current row."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (installed-name nil))
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'config-install-package)
                 (lambda (name) (setq installed-name name)))
                ((symbol-function 'tabulated-list-revert) (lambda ()))
                ((symbol-function 'config--package-installed-p) (lambda (_name) nil)))
        (with-current-buffer (get-buffer-create "*Config Packages Test*")
          (config-packages-mode)
          (setq tabulated-list-entries
                (list (list (list 'magit init-file 1)
                            (vector "magit" "no" init-file "1"))))
          (tabulated-list-print)
          (goto-char (point-min))
          (config-packages-install)
          (should (equal installed-name "magit")))))))

(ert-deftest test-config-packages-install-errors-no-row ()
  "config-packages-install should signal user-error when no package on current row."
  (with-current-buffer (get-buffer-create "*Config Packages Test Empty*")
    (config-packages-mode)
    (setq tabulated-list-entries nil)
    (tabulated-list-print)
    (should-error (config-packages-install) :type 'user-error)))

(ert-deftest test-config-list-packages-install-link-for-uninstalled ()
  "Installed column should contain install button for uninstalled packages."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (popped-buffer nil))
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) nil))
                ((symbol-function 'pop-to-buffer) (lambda (buf &rest _) (setq popped-buffer buf))))
        (config-list-packages)
        (with-current-buffer popped-buffer
          (should (>= (length tabulated-list-entries) 1))
          (let* ((entry (nth 0 tabulated-list-entries))
                 (cols (cadr entry))
                 (installed-col (aref cols 1)))
            (should (string-match-p "install" installed-col))
            (should (string-match-p "no" installed-col))))))))

(ert-deftest test-config-list-packages-remove-link-when-installed ()
  "Installed column should contain remove button for installed packages."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (popped-buffer nil))
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) t))
                ((symbol-function 'pop-to-buffer) (lambda (buf &rest _) (setq popped-buffer buf))))
        (config-list-packages)
        (with-current-buffer popped-buffer
          (should (>= (length tabulated-list-entries) 1))
          (let* ((entry (nth 0 tabulated-list-entries))
                 (cols (cadr entry))
                 (installed-col (aref cols 1)))
            (should (string-match-p "yes" installed-col))
            (should (string-match-p "remove" installed-col))))))))

(ert-deftest test-config-packages-visit-dispatches-install ()
  "config-packages-visit should call config-packages-install when point is on [install] button in Installed column for uninstalled package."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (popped-buffer nil)
          (install-called nil))
      (write-region "(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'config--package-installed-p) (lambda (_name) nil))
                ((symbol-function 'pop-to-buffer) (lambda (buf &rest _) (setq popped-buffer buf)))
                ((symbol-function 'config-install-package)
                 (lambda (_name) (setq install-called t)))
                ((symbol-function 'tabulated-list-revert) (lambda ())))
        (config-list-packages)
        (with-current-buffer popped-buffer
          ;; Move point to the "install" button in the Installed column
          (goto-char (point-min))
          (re-search-forward "install")
          (goto-char (match-beginning 0))
          (config-packages-visit)
          (should install-called))))))

;;; M-x config-list-packages

(ert-deftest test-config-list-packages-creates-buffer ()
  "config-list-packages should create *Config Packages* buffer with correct entries."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (popped-buffer nil))
      (write-region "(use-package magit)\n(use-package org)\n" nil init-file)
      (cl-letf (((symbol-function 'pop-to-buffer) (lambda (buf &rest _) (setq popped-buffer buf))))
        (config-list-packages)
        (should popped-buffer)
        (with-current-buffer popped-buffer
          (should (eq major-mode 'config-packages-mode))
          (should (>= (length tabulated-list-entries) 2))
          (let ((names (mapcar (lambda (e) (symbol-name (nth 0 (car e)))) tabulated-list-entries)))
            (should (member "magit" names))
            (should (member "org" names))))
        (kill-buffer popped-buffer)))))

(ert-deftest test-config-list-packages-empty-config ()
  "config-list-packages should create an empty buffer when no use-package forms exist."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (popped-buffer nil))
      (write-region ";; no packages here\n" nil init-file)
      (cl-letf (((symbol-function 'pop-to-buffer) (lambda (buf &rest _) (setq popped-buffer buf))))
        (config-list-packages)
        (should popped-buffer)
        (with-current-buffer popped-buffer
          (should (eq major-mode 'config-packages-mode))
          (should-not tabulated-list-entries))
        (kill-buffer popped-buffer)))))

(ert-deftest test-config-packages-visit-opens-file-at-line ()
  "config-packages-visit should open the file and jump to the correct line."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (test-config--visited-files nil)
          visited-buffer)
      (write-region ";; header\n\n(use-package magit)\n" nil init-file)
      (cl-letf (((symbol-function 'find-file)
                 (lambda (filename &rest _)
                   (push filename test-config--visited-files)
                   (setq visited-buffer (generate-new-buffer " *test-visit*"))
                   (with-current-buffer visited-buffer
                     (insert-file-contents filename))
                   (switch-to-buffer visited-buffer)
                   visited-buffer)))
        (with-current-buffer (get-buffer-create "*Config Packages Test*")
          (config-packages-mode)
          (setq tabulated-list-entries
                (list (list (list 'magit init-file 3)
                            (vector (propertize "magit" 'face 'link) "yes" init-file "3"))))
          (tabulated-list-print)
          (goto-char (point-min))
          (config-packages-visit))
        (should (= (length test-config--visited-files) 1))
        (should (equal (car test-config--visited-files) init-file))
        (with-current-buffer visited-buffer
          (should (= (line-number-at-pos) 3)))))))

(ert-deftest test-config-packages-mode-keymap ()
  "config-packages-mode should bind expected keys."
  (let ((map config-packages-mode-map))
    (should (equal (lookup-key map (kbd "RET")) 'config-packages-visit))
    (should (equal (lookup-key map (kbd "q")) 'quit-window))))

;;; M-x config-list-non-packages

(ert-deftest test-config--scan-non-packages-finds-forms ()
  "Should find top-level forms that are not use-package."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "test.el" user-emacs-directory)))
      (write-region "(use-package magit)\n(setq x 1)\n(add-hook 'foo 'bar)\n" nil test-file)
      (let ((forms (config--scan-non-packages test-file)))
        (should (= (length forms) 2))
        (should (equal (nth 0 (nth 0 forms)) "(setq x 1)"))
        (should (equal (nth 0 (nth 1 forms)) "(add-hook 'foo 'bar)"))))))

(ert-deftest test-config--scan-non-packages-ignores-use-package ()
  "Should not include use-package forms."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "test.el" user-emacs-directory)))
      (write-region "(use-package magit)\n(use-package org)\n" nil test-file)
      (let ((forms (config--scan-non-packages test-file)))
        (should-not forms)))))

(ert-deftest test-config--scan-non-packages-ignores-comments ()
  "Should ignore forms inside comments."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "test.el" user-emacs-directory)))
      (write-region ";; (setq x 1)\n(setq y 2)\n" nil test-file)
      (let ((forms (config--scan-non-packages test-file)))
        (should (= (length forms) 1))
        (should (equal (nth 0 (nth 0 forms)) "(setq y 2)"))))))

(ert-deftest test-config--scan-non-packages-ignores-empty-file ()
  "Should return nil for an empty file."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "empty.el" user-emacs-directory)))
      (write-region "" nil test-file)
      (should-not (config--scan-non-packages test-file)))))

(ert-deftest test-config--scan-non-packages-line-numbers ()
  "Should report correct line numbers for non-package forms."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "test.el" user-emacs-directory)))
      (write-region ";; header\n\n(setq x 1)\n" nil test-file)
      (let ((forms (config--scan-non-packages test-file)))
        (should (= (length forms) 1))
        (should (= (nth 2 (nth 0 forms)) 3))))))

(ert-deftest test-config-list-non-packages-creates-buffer ()
  "config-list-non-packages should create buffer with correct entries."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (popped-buffer nil))
      (write-region "(use-package magit)\n(setq x 1)\n(add-hook 'foo 'bar)\n" nil init-file)
      (cl-letf (((symbol-function 'pop-to-buffer) (lambda (buf &rest _) (setq popped-buffer buf))))
        (config-list-non-packages)
        (should popped-buffer)
        (with-current-buffer popped-buffer
          (should (eq major-mode 'config-non-packages-mode))
          (should (>= (length tabulated-list-entries) 2)))
        (kill-buffer popped-buffer)))))

(ert-deftest test-config-list-non-packages-empty-config ()
  "config-list-non-packages should create empty buffer when only use-package forms exist."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (popped-buffer nil))
      (write-region "(use-package magit)\n(use-package org)\n" nil init-file)
      (cl-letf (((symbol-function 'pop-to-buffer) (lambda (buf &rest _) (setq popped-buffer buf))))
        (config-list-non-packages)
        (should popped-buffer)
        (with-current-buffer popped-buffer
          (should-not tabulated-list-entries))
        (kill-buffer popped-buffer)))))

(ert-deftest test-config-non-packages-visit-opens-file ()
  "config-non-packages-visit should open the file at the correct line."
  (test-config--with-temp-dir
    (let ((init-file (expand-file-name "init.el" user-emacs-directory))
          (test-config--visited-files nil)
          visited-buffer)
      (write-region "(use-package magit)\n(setq x 1)\n" nil init-file)
      (cl-letf (((symbol-function 'find-file)
                 (lambda (filename &rest _)
                   (push filename test-config--visited-files)
                   (setq visited-buffer (generate-new-buffer " *test-visit*"))
                   (with-current-buffer visited-buffer
                     (insert-file-contents filename))
                   (switch-to-buffer visited-buffer)
                   visited-buffer)))
        (with-current-buffer (get-buffer-create "*Config Non-Packages Test*")
          (config-non-packages-mode)
          (setq tabulated-list-entries
                (list (list (list "(setq x 1)" init-file 2)
                            (vector "(setq x 1)" init-file "2"))))
          (tabulated-list-print)
          (goto-char (point-min))
          (config-non-packages-visit))
        (should (equal (car test-config--visited-files) init-file))
        (with-current-buffer visited-buffer
          (should (= (line-number-at-pos) 2)))))))

(ert-deftest test-config--visit-or-create-opens-existing ()
  "config--visit-or-create should open an existing file."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "existing.el" user-emacs-directory))
          (test-config--visited-files nil))
      (write-region "(content)\n" nil test-file)
      (cl-letf (((symbol-function 'find-file) #'test-config--mock-find-file))
        (config--visit-or-create test-file)
        (should (= (length test-config--visited-files) 1))
        (should (equal (car test-config--visited-files) test-file))))))

(ert-deftest test-config--visit-or-create-creates-when-confirmed ()
  "config--visit-or-create should create file when user confirms."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "new.el" user-emacs-directory))
          (test-config--visited-files nil))
      (cl-letf (((symbol-function 'find-file) #'test-config--mock-find-file)
                ((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (config--visit-or-create test-file)
        (should (file-exists-p test-file))
        (should (= (length test-config--visited-files) 1))
        (should (equal (car test-config--visited-files) test-file))))))

(ert-deftest test-config--visit-or-create-errors-when-declined ()
  "config--visit-or-create should signal user-error when user declines."
  (test-config--with-temp-dir
    (let ((test-file (expand-file-name "new.el" user-emacs-directory)))
      (cl-letf (((symbol-function 'find-file) #'test-config--mock-find-file)
                ((symbol-function 'y-or-n-p) (lambda (_prompt) nil)))
        (should-error (config--visit-or-create test-file) :type 'user-error)))))

;;; M-x config-visit-early-init

(ert-deftest test-config-visit-early-init-opens-existing ()
  "config-visit-early-init should open an existing early-init.el."
  (test-config--with-temp-dir
    (let ((early-init (expand-file-name "early-init.el" user-emacs-directory))
          (test-config--visited-files nil))
      (write-region "(setq gc-cons-threshold 100000000)\n" nil early-init)
      (cl-letf (((symbol-function 'find-file) #'test-config--mock-find-file))
        (config-visit-early-init)
        (should (= (length test-config--visited-files) 1))
        (should (equal (car test-config--visited-files) early-init))))))

(ert-deftest test-config-visit-early-init-creates-when-absent ()
  "config-visit-early-init should create early-init.el when user confirms."
  (test-config--with-temp-dir
    (let ((early-init (expand-file-name "early-init.el" user-emacs-directory))
          (test-config--visited-files nil))
      (cl-letf (((symbol-function 'find-file) #'test-config--mock-find-file)
                ((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (config-visit-early-init)
        (should (file-exists-p early-init))
        (should (= (length test-config--visited-files) 1))
        (should (equal (car test-config--visited-files) early-init))))))

(ert-deftest test-config-visit-early-init-declines-create ()
  "config-visit-early-init should signal user-error when user declines creation."
  (test-config--with-temp-dir
    (let ((early-init (expand-file-name "early-init.el" user-emacs-directory)))
      (cl-letf (((symbol-function 'find-file) #'test-config--mock-find-file)
                ((symbol-function 'y-or-n-p) (lambda (_prompt) nil)))
        (should-error (config-visit-early-init) :type 'user-error)
        (should-not (file-exists-p early-init))))))

;;; M-x config-vcs / config-vcs-setup

(ert-deftest test-config--vcs-emacs-dir-returns-nil-when-not-configured ()
  "Should return nil when config-vcs-directory is nil."
  (let ((config-vcs-directory nil))
    (should-not (config--vcs-emacs-dir))))

(ert-deftest test-config--vcs-emacs-dir-returns-path-when-configured ()
  "Should return .emacs.d/ path inside config-vcs-directory."
  (let ((config-vcs-directory "/tmp/test-vcs"))
    (should (equal (config--vcs-emacs-dir) "/tmp/test-vcs/.emacs.d/"))))

(ert-deftest test-config-vcs-errors-when-directory-missing ()
  "Should signal user-error when config-vcs-directory does not exist."
  (let ((config-vcs-directory "/nonexistent/path/vcs"))
    (should-error (config-vcs) :type 'user-error)))

(ert-deftest test-config-vcs-setup-creates-directory-structure ()
  "Should create .emacs.d/ inside the VCS directory."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (config-vcs-directory nil))
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) nil))
                ((symbol-function 'config--read-vc-backend) (lambda () 'Git)))
        (let ((config-vcs-directory vcs-dir))
          (config-vcs-setup)
          (should (file-directory-p (expand-file-name ".emacs.d" vcs-dir))))))))

(ert-deftest test-config-vcs-setup-initializes-git ()
  "Should initialize a git repository in the VCS directory."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory)))
      (let ((config-vcs-directory vcs-dir))
        (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) nil))
                  ((symbol-function 'config--read-vc-backend) (lambda () 'Git)))
          (config-vcs-setup)
          (should (file-directory-p (expand-file-name ".git" vcs-dir))))))))

(ert-deftest test-config-vcs-setup-sets-config-vcs-directory ()
  "Should set config-vcs-directory after setup."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (config-vcs-directory nil))
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) nil))
                ((symbol-function 'config--read-vc-backend) (lambda () 'Git))
                ((symbol-function 'read-directory-name)
                 (lambda (&rest _) vcs-dir)))
        (config-vcs-setup)
        (should (equal config-vcs-directory (file-name-as-directory vcs-dir)))))))

(ert-deftest test-config-vcs-setup-copies-init-file ()
  "Should copy init.el to VCS .emacs.d/ when user confirms."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (init-file (expand-file-name "init.el" user-emacs-directory))
          (config-vcs-directory nil))
      (write-region "(setq x 1)\n" nil init-file)
      (let ((config-vcs-directory vcs-dir))
        (cl-letf (((symbol-function 'config--read-vc-backend) (lambda () 'Git))
                  ((symbol-function 'y-or-n-p) (lambda (prompt)
                                                   (or (string-match-p "Copy" prompt)
                                                       nil))))
          (config-vcs-setup)
          (should (file-exists-p (expand-file-name ".emacs.d/init.el" vcs-dir))))))))

(ert-deftest test-config--vcs-symlink-config-files-creates-symlinks ()
  "Should create symlinks for .el files in VCS .emacs.d/."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (vcs-emacs-dir nil)
          (init-file nil))
      (setq vcs-emacs-dir (expand-file-name ".emacs.d" vcs-dir))
      (make-directory vcs-emacs-dir t)
      (setq init-file (expand-file-name "init.el" vcs-emacs-dir))
      (write-region "(setq x 1)\n" nil init-file)
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (config--vcs-symlink-config-files (file-name-as-directory vcs-emacs-dir))
        (let ((link-path (expand-file-name "init.el" user-emacs-directory)))
          (should (file-symlink-p link-path))
          (should (equal (file-truename link-path) init-file)))))))

(ert-deftest test-config--vcs-symlink-config-files-skips-existing-when-declined ()
  "Should not replace existing files when user declines."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (vcs-emacs-dir nil)
          (vcs-init nil)
          (local-init nil))
      (setq vcs-emacs-dir (expand-file-name ".emacs.d" vcs-dir))
      (make-directory vcs-emacs-dir t)
      (setq vcs-init (expand-file-name "init.el" vcs-emacs-dir))
      (setq local-init (expand-file-name "init.el" user-emacs-directory))
      (write-region "(vcs)\n" nil vcs-init)
      (write-region "(local)\n" nil local-init)
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) nil)))
        (config--vcs-symlink-config-files (file-name-as-directory vcs-emacs-dir))
        (should-not (file-symlink-p local-init))))))

;;; M-x config-vcs-setup — early-init.el and custom-settings.el

(ert-deftest test-config-vcs-setup-copies-early-init ()
  "Should copy early-init.el to VCS .emacs.d/ when it exists and user confirms."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (init-file (expand-file-name "init.el" user-emacs-directory))
          (early-init (expand-file-name "early-init.el" user-emacs-directory))
          (config-vcs-directory nil))
      (write-region "(setq x 1)\n" nil init-file)
      (write-region "(setq gc-cons-threshold 100000000)\n" nil early-init)
      (let ((config-vcs-directory vcs-dir))
        (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t))
                  ((symbol-function 'config--read-vc-backend) (lambda () 'Git)))
          (config-vcs-setup)
          (should (file-exists-p (expand-file-name ".emacs.d/init.el" vcs-dir)))
          (should (file-exists-p (expand-file-name ".emacs.d/early-init.el" vcs-dir))))))))

(ert-deftest test-config-vcs-setup-copies-custom-settings ()
  "Should copy custom-settings.el to VCS .emacs.d/ when it exists and user confirms."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (init-file (expand-file-name "init.el" user-emacs-directory))
          (custom-file (expand-file-name "custom-settings.el" user-emacs-directory))
          (config-vcs-directory nil))
      (write-region "(setq x 1)\n" nil init-file)
      (write-region "(custom-set-variables)\n" nil custom-file)
      (let ((config-vcs-directory vcs-dir))
        (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t))
                  ((symbol-function 'config--read-vc-backend) (lambda () 'Git)))
          (config-vcs-setup)
          (should (file-exists-p (expand-file-name ".emacs.d/init.el" vcs-dir)))
          (should (file-exists-p (expand-file-name ".emacs.d/custom-settings.el" vcs-dir))))))))

(ert-deftest test-config-vcs-setup-skips-missing-files ()
  "Should not copy early-init.el or custom-settings.el when they do not exist."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (init-file (expand-file-name "init.el" user-emacs-directory))
          (config-vcs-directory nil))
      (write-region "(setq x 1)\n" nil init-file)
      (let ((config-vcs-directory vcs-dir))
        (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t))
                  ((symbol-function 'config--read-vc-backend) (lambda () 'Git)))
          (config-vcs-setup)
          (should (file-exists-p (expand-file-name ".emacs.d/init.el" vcs-dir)))
          (should-not (file-exists-p (expand-file-name ".emacs.d/early-init.el" vcs-dir)))
          (should-not (file-exists-p (expand-file-name ".emacs.d/custom-settings.el" vcs-dir))))))))

(ert-deftest test-config--vcs-maybe-copy-copies-when-source-exists ()
  "Should copy file when source exists, destination absent, and user confirms."
  (test-config--with-temp-dir
    (let ((src (expand-file-name "source.el" user-emacs-directory))
          (dst (expand-file-name "dest.el" user-emacs-directory)))
      (write-region "(content)\n" nil src)
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (config--vcs-maybe-copy src dst)
        (should (file-exists-p dst))
        (should (equal (with-temp-buffer (insert-file-contents dst) (buffer-string))
                       "(content)\n"))))))

(ert-deftest test-config--vcs-maybe-copy-skips-missing-source ()
  "Should do nothing when source file does not exist."
  (test-config--with-temp-dir
    (let ((src (expand-file-name "nonexistent.el" user-emacs-directory))
          (dst (expand-file-name "dest.el" user-emacs-directory)))
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (config--vcs-maybe-copy src dst)
        (should-not (file-exists-p dst))))))

(ert-deftest test-config--vcs-maybe-copy-skips-existing-destination ()
  "Should not overwrite an existing destination file."
  (test-config--with-temp-dir
    (let ((src (expand-file-name "source.el" user-emacs-directory))
          (dst (expand-file-name "dest.el" user-emacs-directory)))
      (write-region "(source)\n" nil src)
      (write-region "(dest)\n" nil dst)
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (config--vcs-maybe-copy src dst)
        (should (equal (with-temp-buffer (insert-file-contents dst) (buffer-string))
                       "(dest)\n"))))))

(ert-deftest test-config--vcs-symlink-config-files-symlinks-early-init ()
  "Should create symlink for early-init.el in VCS .emacs.d/."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (vcs-emacs-dir nil)
          (init-file nil)
          (early-init-file nil))
      (setq vcs-emacs-dir (expand-file-name ".emacs.d" vcs-dir))
      (make-directory vcs-emacs-dir t)
      (setq init-file (expand-file-name "init.el" vcs-emacs-dir))
      (setq early-init-file (expand-file-name "early-init.el" vcs-emacs-dir))
      (write-region "(setq x 1)\n" nil init-file)
      (write-region "(setq gc-cons-threshold 100000000)\n" nil early-init-file)
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (config--vcs-symlink-config-files (file-name-as-directory vcs-emacs-dir))
        (should (file-symlink-p (expand-file-name "init.el" user-emacs-directory)))
        (should (file-symlink-p (expand-file-name "early-init.el" user-emacs-directory)))
        (should (equal (file-truename (expand-file-name "early-init.el" user-emacs-directory)) early-init-file))))))

;;; M-x config-vcs-add-files

(ert-deftest test-config-vcs-add-files-errors-when-not-configured ()
  "Should signal user-error when config-vcs-directory is nil."
  (let ((config-vcs-directory nil))
    (should-error (config-vcs-add-files) :type 'user-error)))

(ert-deftest test-config-vcs-add-files-errors-when-outside-home ()
  "Should signal user-error when file is not under home directory."
  (test-config--with-temp-dir
    (let ((config-vcs-directory user-emacs-directory))
      (cl-letf (((symbol-function 'read-file-name)
                 (lambda (&rest _) "/tmp/outside-home-file.el")))
        (should-error (config-vcs-add-files) :type 'user-error)))))

(ert-deftest test-config--vcs-add-file-copies-and-symlinks ()
  "Should copy file to VCS repo and replace original with symlink."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (src-file (expand-file-name "test-file.el" user-emacs-directory))
          (dst-file nil)
          (config-vcs-directory nil))
      (setq config-vcs-directory vcs-dir)
      (make-directory vcs-dir t)
      (setq dst-file (expand-file-name "test-file.el" vcs-dir))
      (write-region "(content)\n" nil src-file)
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (config--vcs-add-file src-file dst-file)
        (should (file-exists-p dst-file))
        (should (file-symlink-p src-file))
        (should (equal (file-truename src-file) dst-file))))))

(ert-deftest test-config--vcs-add-file-declines-symlink ()
  "Should copy file but not replace with symlink when user declines."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (src-file (expand-file-name "test-file.el" user-emacs-directory))
          (dst-file nil)
          (config-vcs-directory nil))
      (setq config-vcs-directory vcs-dir)
      (make-directory vcs-dir t)
      (setq dst-file (expand-file-name "test-file.el" vcs-dir))
      (write-region "(content)\n" nil src-file)
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (prompt)
                                                   (not (string-match-p "Replace" prompt)))))
        (config--vcs-add-file src-file dst-file)
        (should (file-exists-p dst-file))
        (should-not (file-symlink-p src-file))))))

(ert-deftest test-config--vcs-add-file-declines-overwrite ()
  "Should not overwrite existing destination when user declines."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (src-file (expand-file-name "test-file.el" user-emacs-directory))
          (dst-file nil)
          (config-vcs-directory nil))
      (setq config-vcs-directory vcs-dir)
      (make-directory vcs-dir t)
      (setq dst-file (expand-file-name "test-file.el" vcs-dir))
      (write-region "(source)\n" nil src-file)
      (write-region "(dest)\n" nil dst-file)
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (prompt)
                                                   (not (string-match-p "Overwrite" prompt)))))
        (config--vcs-add-file src-file dst-file)
        (should (equal (with-temp-buffer (insert-file-contents dst-file) (buffer-string))
                       "(dest)\n"))))))

(ert-deftest test-config--vcs-add-directory-copies-and-symlinks ()
  "Should copy all files from directory and create 1-to-1 symlinks."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (src-dir (expand-file-name ".config/app" user-emacs-directory))
          (dst-dir nil)
          (config-vcs-directory nil))
      (setq config-vcs-directory vcs-dir)
      (make-directory vcs-dir t)
      (make-directory src-dir t)
      (make-directory (expand-file-name "subdir" src-dir) t)
      (write-region "(setting1)\n" nil (expand-file-name "config.el" src-dir))
      (write-region "(setting2)\n" nil (expand-file-name "subdir/extra.el" src-dir))
      (setq dst-dir (expand-file-name ".config/app" vcs-dir))
      (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
        (config--vcs-add-directory src-dir dst-dir)
        (should (file-exists-p (expand-file-name "config.el" dst-dir)))
        (should (file-exists-p (expand-file-name "subdir/extra.el" dst-dir)))
        (should (file-symlink-p (expand-file-name "config.el" src-dir)))
        (should (file-symlink-p (expand-file-name "subdir/extra.el" src-dir)))))))

;;; M-x config-remote

(ert-deftest test-config--remote-url-valid-p-accepts-https ()
  "Should accept https URLs."
  (should (config--remote-url-valid-p "https://github.com/user/repo.git"))
  (should (config--remote-url-valid-p "https://gitlab.com/user/repo"))
  (should (config--remote-url-valid-p "http://example.com/repo.git")))

(ert-deftest test-config--remote-url-valid-p-accepts-ssh ()
  "Should accept ssh URLs."
  (should (config--remote-url-valid-p "git@github.com:user/repo.git"))
  (should (config--remote-url-valid-p "ssh://git@example.com/repo.git"))
  (should (config--remote-url-valid-p "git@gitlab.com:user/repo")))

(ert-deftest test-config--remote-url-valid-p-rejects-invalid ()
  "Should reject invalid URLs."
  (should-not (config--remote-url-valid-p "ftp://example.com/repo"))
  (should-not (config--remote-url-valid-p "not-a-url"))
  (should-not (config--remote-url-valid-p ""))
  (should-not (config--remote-url-valid-p "file:///local/repo")))

;;; M-x config-clone

(ert-deftest test-config-clone-clones-repository ()
  "Should clone repository and set config-vcs-directory."
  (let ((config-vcs-directory nil)
        (clone-dir (make-temp-file "config-el-clone" t))
        (clone-called-with nil))
    (cl-letf (((symbol-function 'read-directory-name)
               (lambda (_prompt _dir &rest _) clone-dir))
              ((symbol-function 'read-string)
               (lambda (_prompt &rest _) ""))
              ((symbol-function 'config--vc-clone)
               (lambda (url dir)
                 (setq clone-called-with (list url dir))
                 dir)))
      (config-clone "https://github.com/user/repo.git")
      (should config-vcs-directory)
      (should (equal (car clone-called-with) "https://github.com/user/repo.git"))
      (should (equal (cadr clone-called-with) clone-dir)))))

(ert-deftest test-config-clone-errors-when-clone-fails ()
  "Should signal user-error when clone fails."
  (let ((config-vcs-directory nil))
    (cl-letf (((symbol-function 'read-directory-name)
               (lambda (_prompt _dir &rest _) (make-temp-file "config-el-fail" t)))
              ((symbol-function 'read-string) (lambda (_prompt) ""))
              ((symbol-function 'config--vc-clone) (lambda (_url _dir) nil)))
      (should-error (config-clone "https://github.com/user/repo.git") :type 'user-error))))

(ert-deftest test-config-clone-rejects-invalid-url ()
  "Should reject invalid remote URLs."
  (should-error (config-clone "not-a-url") :type 'user-error))

;;; M-x config-remote

(ert-deftest test-config-remote-errors-when-no-vcs-dir ()
  "Should signal user-error when config-vcs-directory is nil."
  (let ((config-vcs-directory nil))
    (should-error (config-remote "https://github.com/user/repo.git") :type 'user-error)))

(ert-deftest test-config-remote-errors-when-directory-missing ()
  "Should signal user-error when config-vcs-directory does not exist."
  (let ((config-vcs-directory "/nonexistent/vcs/dir"))
    (should-error (config-remote "https://github.com/user/repo.git") :type 'user-error)))

(ert-deftest test-config-remote-errors-when-no-backend ()
  "Should signal user-error when no VCS backend found in directory."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (config-vcs-directory nil))
      (make-directory vcs-dir t)
      (setq config-vcs-directory vcs-dir)
      (should-error (config-remote "https://github.com/user/repo.git") :type 'user-error))))

(ert-deftest test-config-remote-rejects-invalid-url ()
  "Should reject invalid remote URLs."
  (should-error (config-remote "not-a-url") :type 'user-error))

(ert-deftest test-config-remote-adds-origin ()
  "Should add 'origin' remote when none exists."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (remote-url "https://github.com/user/emacs-config.git")
          (config-vcs-directory nil))
      (make-directory vcs-dir t)
      (let ((default-directory vcs-dir)) (vc-git-command nil 0 nil "init"))
      (setq config-vcs-directory vcs-dir)
      (config-remote remote-url)
      (let ((output (with-temp-buffer
                      (let ((default-directory vcs-dir)) (vc-git-command (current-buffer) 0 nil "remote" "-v"))
                      (buffer-string))))
        (should (string-match-p "origin" output))
        (should (string-match-p (regexp-quote remote-url) output))))))

(ert-deftest test-config-remote-sets-url-when-origin-exists ()
  "Should set-url when origin remote already exists."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (old-url "https://github.com/old/repo.git")
          (new-url "https://github.com/new/repo.git")
          (config-vcs-directory nil))
      (make-directory vcs-dir t)
      (let ((default-directory vcs-dir)) (vc-git-command nil 0 nil "init"))
      (let ((default-directory vcs-dir)) (vc-git-command nil 0 nil "remote" "add" "origin" old-url))
      (setq config-vcs-directory vcs-dir)
      (config-remote new-url)
      (let ((output (with-temp-buffer
                      (let ((default-directory vcs-dir)) (vc-git-command (current-buffer) 0 nil "remote" "-v"))
                      (buffer-string))))
        (should (string-match-p (regexp-quote new-url) output))
        (should-not (string-match-p (regexp-quote old-url) output))))))

;;; M-x config-commit

(ert-deftest test-config-commit-errors-when-no-vcs-dir ()
  "Should signal user-error when no VCS directory configured."
  (let ((config-vcs-directory nil))
    (should-error (config-commit) :type 'user-error)))

(ert-deftest test-config-commit-errors-when-directory-missing ()
  "Should signal user-error when VCS directory does not exist."
  (let ((config-vcs-directory "/nonexistent/vcs/dir"))
    (should-error (config-commit) :type 'user-error)))

(ert-deftest test-config-commit-errors-when-no-backend ()
  "Should signal user-error when no VCS backend found in directory."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (config-vcs-directory nil))
      (make-directory vcs-dir t)
      (setq config-vcs-directory vcs-dir)
      (should-error (config-commit) :type 'user-error))))

(ert-deftest test-config-commit-commits-staged-changes ()
  "Should stage all changes and commit with the given message."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (config-vcs-directory nil))
      (make-directory vcs-dir t)
      (let ((default-directory vcs-dir))
        (vc-git-command nil 0 nil "init")
        (vc-git-command nil 0 nil "config" "user.name" "Test")
        (vc-git-command nil 0 nil "config" "user.email" "test@test.com"))
      (write-region "initial" nil (expand-file-name "test.txt" vcs-dir))
      (let ((default-directory vcs-dir))
        (vc-git-command nil 0 nil "add" "test.txt")
        (vc-git-command nil 0 nil "commit" "-m" "initial"))
      (write-region "modified" nil (expand-file-name "test.txt" vcs-dir))
      (setq config-vcs-directory vcs-dir)
      (cl-letf (((symbol-function 'read-string) (lambda (_prompt) "test commit"))
                ((symbol-function 'vc-checkin)
                 (lambda (files backend msg)
                   (let ((default-directory config-vcs-directory))
                     (vc-git-command nil 0 nil "add" "-A")
                     (vc-git-command nil 0 nil "commit" "-m" msg)))))
        (config-commit))
      (let ((output (with-temp-buffer
                      (let ((default-directory vcs-dir))
                        (vc-git-command (current-buffer) 0 nil "log" "--oneline"))
                      (buffer-string))))
        (should (string-match-p "test commit" output))))))

;;; M-x config-push

(ert-deftest test-config-push-errors-when-no-vcs-dir ()
  "Should signal user-error when no VCS directory configured."
  (let ((config-vcs-directory nil))
    (should-error (config-push) :type 'user-error)))

(ert-deftest test-config-push-errors-when-directory-missing ()
  "Should signal user-error when VCS directory does not exist."
  (let ((config-vcs-directory "/nonexistent/vcs/dir"))
    (should-error (config-push) :type 'user-error)))

(ert-deftest test-config-push-errors-when-no-backend ()
  "Should signal user-error when no VCS backend found in directory."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (config-vcs-directory nil))
      (make-directory vcs-dir t)
      (setq config-vcs-directory vcs-dir)
      (should-error (config-push) :type 'user-error))))

;;; M-x config-diff

(ert-deftest test-config-diff-finds-changed-files ()
  "Should find modified files in the config VCS repository."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (config-vcs-directory nil))
      (make-directory vcs-dir t)
      (let ((default-directory vcs-dir))
        (vc-git-command nil 0 nil "init")
        (vc-git-command nil 0 nil "config" "user.name" "Test")
        (vc-git-command nil 0 nil "config" "user.email" "test@test.com"))
      (write-region "initial\n" nil (expand-file-name "test.txt" vcs-dir))
      (let ((default-directory vcs-dir))
        (vc-git-command nil 0 nil "add" "test.txt")
        (vc-git-command nil 0 nil "commit" "-m" "initial"))
      (write-region "modified\n" nil (expand-file-name "test.txt" vcs-dir))
      (setq config-vcs-directory vcs-dir)
      (let* ((backend (vc-responsible-backend vcs-dir))
             (files (config--vc-changed-files vcs-dir backend)))
        (should (>= (length files) 1))
        (should (member (expand-file-name "test.txt" vcs-dir) files))))))

(ert-deftest test-config-diff-errors-when-no-vcs-dir ()
  "Should signal user-error when no VCS directory configured."
  (let ((config-vcs-directory nil))
    (should-error (config-diff) :type 'user-error)))

(ert-deftest test-config-diff-errors-when-directory-missing ()
  "Should signal user-error when VCS directory does not exist."
  (let ((config-vcs-directory "/nonexistent/vcs/dir"))
    (should-error (config-diff) :type 'user-error)))

(ert-deftest test-config-diff-errors-when-no-backend ()
  "Should signal user-error when no VCS backend found in directory."
  (test-config--with-temp-dir
    (let ((vcs-dir (expand-file-name "vcs" user-emacs-directory))
          (config-vcs-directory nil))
      (make-directory vcs-dir t)
      (setq config-vcs-directory vcs-dir)
      (should-error (config-diff) :type 'user-error))))

;;; test-config.el ends here
