;;; +django-tests.el -*- lexical-binding: t; -*-

(require 'projectile)
(require 'python)
(require 'subr-x)

(defun django--project-root ()
  (or (projectile-project-root)
      (user-error "Not in a Projectile project")))

(defun django--module-from-file (file root)
  (let* ((rel (file-relative-name file root))
         (no-ext (file-name-sans-extension rel)))
    (replace-regexp-in-string "/" "." no-ext)))

(defun django--def-name-at-point ()
  (save-excursion
    (when (ignore-errors (python-nav-beginning-of-defun) t)
      (when (looking-at (rx bol (* space) "def" (+ space)
                            (group (+ (or word ?_))) (* space) "("))
        (match-string-no-properties 1)))))

(defun django--enclosing-class-name ()
  (save-excursion
    (let ((found nil)
          (limit (point-min)))
      (while (and (not found) (> (point) limit))
        (python-nav-beginning-of-statement)
        (when (looking-at (rx bol (* space) "class" (+ space)
                              (group (+ (or word ?_)))))
          (setq found (match-string-no-properties 1)))
        (condition-case _err
            (python-nav-backward-block)
          (error (goto-char limit))))
      found)))

(defun django--selector-at-point ()
  (let ((fn (django--def-name-at-point))
        (cls (django--enclosing-class-name)))
    (cond
     ((and fn (string-prefix-p "test_" fn))
      (if cls (format "%s.%s" cls fn) fn))
     (cls cls)
     (t nil))))

(defun django--ensure-env (root)
  (let ((default-directory root))
    (when (fboundp 'envrc-reload)
      (envrc-reload))))

(defface django-test-command-face
  '((t :weight bold :foreground "orange" :height 1.15))
  "Face used for Django test commands.")

(defun django--insert-command-header (buf root command)
  "Insert a loud command header at the top of BUF."
  (with-current-buffer (get-buffer-create buf)
    (let ((inhibit-read-only t))
      (save-excursion
        (goto-char (point-min))
        (insert (propertize (make-string 72 ?═) 'face 'shadow) "\n")
        (insert (propertize
                 (format "$ (cd %s && %s)\n" root command)
                 'face 'django-test-command-face))
        (insert (propertize (make-string 72 ?═) 'face 'shadow) "\n\n")))))

(defun django-run-tests (&optional module selector)
  (interactive)
  (let* ((root (django--project-root))
         (module-part (or module ""))
         (selector-part (if selector (concat "." selector) ""))
         (param (string-trim (concat module-part selector-part)))
         (command (format "python manage.py test --keepdb %s" param))
         (buf "*Django tests*"))
    ;; Ensure envrc/direnv is current
    (let ((default-directory root))
      (when (fboundp 'envrc-reload) (envrc-reload)))

    ;; Start the process (this may clear/initialize the buffer)
    (projectile-run-async-shell-command-in-root command buf)

    ;; Now insert header at top (won't get wiped)
    (django--insert-command-header buf root command)

    (pop-to-buffer buf)))

(defun django-run-test-at-point ()
  (interactive)
  (let* ((root (django--project-root))
         (file (or (buffer-file-name) (user-error "Buffer is not visiting a file")))
         (module (django--module-from-file file root))
         (sel (django--selector-at-point)))
    (django-run-tests module sel)))

(defun django-run-tests-for-current-file ()
  (interactive)
  (let* ((root (django--project-root))
         (file (or (buffer-file-name) (user-error "Buffer is not visiting a file")))
         (module (django--module-from-file file root)))
    (django-run-tests module nil)))

(provide '+django-tests)
