;;; -*- mode: Lisp; Syntax: Common-Lisp; -*-
;;;
;;; Copyright (c) 2006 by the authors.
;;;
;;; See LICENCE for details.

(in-package :swank)

;; The Slime maintainer is hostile to these, so we define this unconditionally for ourselves, so that
;; it works on Slime HEAD, too.

(defun hu.dwim.stefil::label-value-line (label value &key padding-length display-nil-value hide-when-nil
                                         splice-as-ispec value-text (newline t))
  "Create a control list which prints \"LABEL: VALUE\" in the inspector."
  (if (or value (not hide-when-nil))
      `((:label ,(princ-to-string label) ":")
        ,@(when (or value display-nil-value)
                (list " "))
        ,@(when (and (or value display-nil-value)
                     padding-length)
                (list (make-array padding-length
                                  :element-type 'character
                                  :initial-element #\Space)))
        ,@(when (or value display-nil-value)
                (if splice-as-ispec
                    (if (listp value) value (list value))
                    `((:value ,value ,@(when value-text (list value-text))))))
        ,@(if newline '((:newline)) nil))
      (values)))

(defmacro hu.dwim.stefil::label-value-line* (&rest label-values)
  (let ((longest-label-length (loop
                                 :for (label value) :in label-values
                                 :maximize (if (stringp label)
                                               (length label)
                                               0))))
  `(append ,@(loop
                :for entry :in label-values
                :if (and (consp entry)
                         (not (consp (first entry)))
                         (string= (first entry) '@))
                  :appending (rest entry)
                :else
                  :collect (destructuring-bind
                                 (label value &rest args &key &allow-other-keys) entry
                             `(hu.dwim.stefil::label-value-line
                               ,label ,value
                               :padding-length ,(when (stringp label)
                                                      (- longest-label-length
                                                         (length label)))
                               ,@args))))))

(in-package :hu.dwim.stefil)

(defun call-with-sldb-quit-restart (thunk restart)
  (let* ((swank::*sldb-quit-restart* restart))
    (funcall thunk)))

(defun stefil-inspector-lookup-hook (form)
  (when (symbolp form)
    (let ((test (find-test form :otherwise nil)))
      (when test
        (values test t)))))

;; as of this writing the official slime does not support inspector lookup hooks.
;; see patch "slime-inspect got a :mode argument that controls its behavior."
;; in the branch: https://github.com/attila-lendvai/slime
(when (boundp 'swank::*inspector-lookup-hooks*)
  (pushnew 'stefil-inspector-lookup-hook (symbol-value 'swank::*inspector-lookup-hooks*)))

(defvar *display-all-slots-in-inspector* nil)

(defun make-rerun-test-action-for-inspector (context)
  (lambda ()
    (apply (name-of (test-of context))
           (mapcar #'cdr (test-arguments-of context)))
    (swank::inspect-object *last-test-result*)))

(defun make-run-test-action-for-inspector (test)
  (lambda ()
    (funcall (name-of test))
    (swank::inspect-object *last-test-result*)))

(defun present-test-for-emacs (test &key name-only undefine-action actions-first)
  (when test
    (let* ((actions `(,@(if (lambda-list-of test)
                            (when actions-first
                              `((:label "     ")))
                            `((:action "[run]" ,(make-run-test-action-for-inspector test))))
                      ,@(when undefine-action
                          (if (find-test (name-of test) :otherwise nil)
                              (when actions-first
                                `((:label "          ")))
                              `((:action "[undefine]" ,(lambda () (setf (find-test (name-of test)) nil))))))))
           (value `((:value ,(if name-only (name-of test) test)))))
      (if actions-first
          (append actions '(" ") value)
          (append value '(" ") actions)))))

(defun present-context-for-emacs (context)
  `((:value ,context) " " (:action "[rerun]" ,(make-rerun-test-action-for-inspector context))))

(defun present-test-backtrace-for-emacs (description)
  (loop
    :with first-time? = t
    :for context :in (test-context-backtrace-of description)
    :for idx :upfrom 0
    :when first-time?
      :do (setf first-time? nil)
      :and :appending `((:newline) (:label "Test backtrace:") (:newline))
    :collect (format nil "  ~D: " idx)
    :appending (present-context-for-emacs context)
    :collect `(:newline)))

(defun present-all-slots-for-emacs (object)
  (if *display-all-slots-in-inspector*
      (append `((:newline)
                (:action "[hide slots]" ,(lambda () (setf *display-all-slots-in-inspector* nil)))
                (:newline))
              (swank::all-slots-for-inspector object))
      `((:newline)
        (:action "[show all slots]" ,(lambda () (setf *display-all-slots-in-inspector* t))))))

(defmacro inspector-result (title content)
  (declare (ignore title))
  ;;`(list :title ,title :type nil :content ,content)
  content)

(defmethod swank-backend::emacs-inspect ((global-context global-context))
  (inspector-result
   "Stefil test results"
   (label-value-line*
    ("Executed tests" (hash-table-values (run-tests-of global-context))
                      :value-text (princ-to-string (hash-table-count (run-tests-of global-context))))
    ("Executed assertions" (princ-to-string (assertion-count-of global-context)) :splice-as-ispec t)
    (@ (unless (emptyp (failure-descriptions-of global-context))
         `((:label ,(format nil "List of failures (~A): " (length (failure-descriptions-of global-context))))
           (:action "[rerun all failed tests]"
                    ,(lambda () (swank::inspect-object (run-failed-tests global-context))))
           (:newline))))
    ;; intentionally reverse the order by push'ing
    (@ (loop
         :for description :across (failure-descriptions-of global-context)
         :for context = (first (test-context-backtrace-of description))
         :collect "  "
         :collect `(:action "[rerun]" ,(make-rerun-test-action-for-inspector context))
         :collect " "
         :collect `(:value ,description)
         :collect `(:newline)))
    (@ (present-all-slots-for-emacs global-context)))))

(defmethod swank-backend::emacs-inspect ((context context))
  (inspector-result
   "Stefil test context"
   (label-value-line*
    ("Test" (test-of context))
    ("Test arguments" (test-arguments-of context) :display-nil-value nil)
    ("Real time spent in body" (let* ((time-spent (real-time-spent-in-seconds context)))
                                 (list (if time-spent (format nil "~,3F" time-spent) "?")
                                       '(:label " sec ")
                                       `(:action "[rerun]" ,(make-rerun-test-action-for-inspector context))))
                               :splice-as-ispec t)
    (@ (loop
         :with first-time? = t
         :for parent-context = (parent-context-of context) :then (parent-context-of parent-context)
         :while parent-context
         :when first-time?
           :do (setf first-time? nil)
           :and :collect `(:label "Parent test frames:")
           :and :collect `(:newline)
         :collect "  "
         :appending (reverse (present-context-for-emacs parent-context))
         :collect `(:newline)))
    (@ (present-all-slots-for-emacs context)))))

(defmethod swank-backend::emacs-inspect ((failure failed-assertion))
  (inspector-result
   "Failed Stefil assertion"
   (label-value-line*
    ("Form" (form-of failure))
    (@ (present-test-backtrace-for-emacs failure))
    (@ (present-all-slots-for-emacs failure)))))

(defmethod swank-backend::emacs-inspect ((description unexpected-error))
  (inspector-result
   "Unexpected error in a Stefil test"
   (label-value-line*
    ("Condition" (condition-of description))
    (@ (present-test-backtrace-for-emacs description))
    (@ (present-all-slots-for-emacs description)))))

(defmethod swank-backend::emacs-inspect ((test test))
  (inspector-result
   "Stefil test"
   (label-value-line*
    ("Name" (present-test-for-emacs test :undefine-action t :name-only t) :splice-as-ispec t)
    ("Package" (package-of test))
    ("Compile before run?" (if (compile-before-run-p test) "yes" "no") :splice-as-ispec t)
    ("Auto call by its suite?" (if (auto-call? test) "yes" "no") :splice-as-ispec t)
    ("Documentation" (documentation-of test) :display-nil-value nil)
    ("Parent" (present-test-for-emacs (parent-of test)) :splice-as-ispec t)
    (@ `((:newline)
         (:action "[undefine]" ,(lambda () (setf (find-test test) nil)))
         (:newline)))
    (@ (loop
         :with first-time? = t
         :for child :being :the :hash-values :of (children-of test)
         :when first-time?
           :do (setf first-time? nil)
           :and :appending `((:newline) (:label "Children:") (:newline))
         :collect "  "
         :appending (present-test-for-emacs child :actions-first t)
         :collect `(:newline)))
    (@ (present-all-slots-for-emacs test)))))
