;;; ai-code-harness.el --- Harness support for ai-code -*- lexical-binding: t; -*-

;; Author: Kang Tu <tninja@gmail.com>

;; SPDX-License-Identifier: Apache-2.0

;;; Commentary:
;; Prompt clarification, harness generation, and send-time routing for ai-code.

;;; Code:

(require 'seq)
(require 'subr-x)

(defvar ai-code--harness-loading nil
  "Non-nil while harness loading must be deferred to avoid dependency cycles.")

(let ((ai-code--harness-loading t))
  (require 'ai-code-agile)
  (require 'ai-code-backends)
  (require 'ai-code-change)
  (require 'ai-code-discussion)
  (require 'ai-code-github)
  (require 'ai-code-prompt-mode))

(declare-function ai-code--git-root "ai-code-utils" (&optional dir))
(declare-function ai-code-call-gptel-sync "ai-code-prompt-mode" (question))
(declare-function ai-code-prompt-context-cache
                  "ai-code-prompt-mode" (context))
(declare-function ai-code-prompt-context-memoize
                  "ai-code-prompt-mode" (context key producer))
(declare-function ai-code-prompt-context-origin-command
                  "ai-code-prompt-mode" (context))
(declare-function ai-code-prompt-context-prompt-text
                  "ai-code-prompt-mode" (context))

(defvar ai-code--prompt-origin-command)
(defvar ai-code--high-value-tests-instruction)
(defvar ai-code--tdd-red-green-base-instruction)
(defvar ai-code--tdd-red-green-tail-instruction)
(defvar ai-code--tdd-run-test-after-each-stage-instruction)
(defvar ai-code--tdd-test-pattern-instruction)
(defvar ai-code--tdd-with-refactoring-extension-instruction)
(defvar ai-code-change--ask-question-note)
(defvar ai-code-change--generic-note)
(defvar ai-code-change--question-brief-default-boundaries)
(defvar ai-code-change--selected-files-note)
(defvar ai-code-change--selected-region-note)
(defvar ai-code-discussion--exception-investigation-boundaries)
(defvar ai-code-discussion--explain-prompt-prefixes)
(defvar ai-code-discussion--question-only-note)
(defvar ai-code-discussion--selected-region-note)
(defvar ai-code-github--analysis-only-notes)
(defvar ai-code-mcp-agent-enabled-backends)
(defvar ai-code-prompt-suffix-functions)
(defvar ai-code-selected-backend)
(defvar ai-code-use-prompt-suffix)

;;;; Auto-Test Harness: Content and Cache

(defconst ai-code--diagnostics-first-harness-instruction
  "Before editing, record a diagnostics baseline by calling the diagnostics_baseline MCP tool. After each edit, call the get_diagnostics MCP tool with since=\"baseline\" for the touched files and do not finish until its status is \"clean\" (no new diagnostics versus the baseline)."
  "Shared diagnostics-first harness guidance for code-change prompts.")

(defun ai-code--diagnostics-first-harness-instruction-inline ()
  "Return diagnostics-first guidance formatted for inline prompt text."
  (concat (downcase (substring ai-code--diagnostics-first-harness-instruction 0 1))
          (substring ai-code--diagnostics-first-harness-instruction 1)))

(defcustom ai-code-test-after-code-change-suffix
  (concat
   "If any program code changes, run unit-tests and follow up on the test-result (fix code if there is an error). "
   ai-code--high-value-tests-instruction
   " If the tests use random values (for example random numbers or UUIDs), make them reproducible by fixing the random seed or replacing them with deterministic fixtures.")
  "User-provided prompt suffix for test-after-code-change."
  :type '(choice (const nil) string)
  :group 'ai-code)

(defconst ai-code--auto-test-harness-file-version "v1"
  "Version tag appended to generated auto-test harness file names.")

(defun ai-code--package-directory ()
  "Return the package installation directory for ai-code."
  (file-name-directory
   (file-truename
    (or (locate-library "ai-code")
        load-file-name
        buffer-file-name
        default-directory))))

(defun ai-code--auto-test-harness-directory ()
  "Return the package installation `prompt/' directory for harness files."
  (expand-file-name "prompt/" (ai-code--package-directory)))

(defun ai-code--auto-test-harness-prompt-path (file-path)
  "Return FILE-PATH formatted for prompt usage.
When FILE-PATH is inside the current git repository, return a repo-relative
path; otherwise return the absolute FILE-PATH."
  (if-let ((git-root (ai-code--git-root)))
      (let ((git-root-truename (file-name-as-directory (file-truename git-root)))
            (file-truename (file-truename file-path)))
        (if (file-in-directory-p file-truename git-root-truename)
            (file-relative-name file-truename git-root-truename)
          file-path))
    file-path))

(defun ai-code--auto-test-backend ()
  "Return the backend symbol used for auto-test prompt decisions."
  (if (fboundp 'ai-code--effective-backend)
      (or (ai-code--effective-backend) ai-code-selected-backend)
    ai-code-selected-backend))

(defun ai-code--diagnostics-harness-enabled-p ()
  "Return non-nil when the current backend should get diagnostics guidance."
  (memq (ai-code--auto-test-backend)
        ai-code-mcp-agent-enabled-backends))

(defun ai-code--maybe-append-diagnostics-harness-instruction (suffix &optional inline)
  "Append diagnostics harness guidance to SUFFIX when the backend supports it.
When INLINE is non-nil, use the inline-formatted diagnostics instruction."
  (if (and (stringp suffix)
           (> (length suffix) 0)
           (ai-code--diagnostics-harness-enabled-p))
      (let ((instruction (if inline
                             (ai-code--diagnostics-first-harness-instruction-inline)
                           ai-code--diagnostics-first-harness-instruction)))
        (concat suffix
                (if inline " " "")
                instruction))
    suffix))

(defun ai-code--test-after-code-change--resolve-tdd-suffix ()
  "Return the TDD-style suffix for test-after-code-change prompt text."
  (ai-code--maybe-append-diagnostics-harness-instruction
   (concat ai-code--tdd-red-green-base-instruction
           ai-code--tdd-red-green-tail-instruction
           ai-code--tdd-run-test-after-each-stage-instruction
           ai-code--tdd-test-pattern-instruction)))

(defun ai-code--test-after-code-change--resolve-tdd-with-refactoring-suffix ()
  "Return the TDD+refactoring suffix for test-after-code-change prompt text."
  (ai-code--maybe-append-diagnostics-harness-instruction
   (concat ai-code--tdd-red-green-base-instruction
           ai-code--tdd-with-refactoring-extension-instruction
           ai-code--tdd-red-green-tail-instruction
           ai-code--tdd-run-test-after-each-stage-instruction
           ai-code--tdd-test-pattern-instruction)))

(defun ai-code--auto-test-inline-suffix-for-type (type)
  "Return the inline prompt suffix for auto test TYPE."
  (pcase type
    ('test-after-change
     (ai-code--maybe-append-diagnostics-harness-instruction
      ai-code-test-after-code-change-suffix t))
    ('tdd (ai-code--test-after-code-change--resolve-tdd-suffix))
    ('tdd-with-refactoring (ai-code--test-after-code-change--resolve-tdd-with-refactoring-suffix))
    ('no-test "Do not write or run any test.")
    (_ nil)))

(defun ai-code--auto-test-harness-file-name (type)
  "Return the stable harness file name for auto test TYPE."
  (let ((base-name (symbol-name type)))
    (format "%s%s.%s.md"
            base-name
            (if (ai-code--diagnostics-harness-enabled-p)
                "-diagnostics"
              "")
            ai-code--auto-test-harness-file-version)))

(defun ai-code--ensure-auto-test-harness-prompt-directory ()
  "Ensure the package prompt directory exists and return it."
  (let ((directory (ai-code--auto-test-harness-directory)))
    (unless (file-directory-p directory)
      (make-directory directory t))
    directory))

(defun ai-code--auto-test-harness-text-for-type (type)
  "Return the externalized harness text for auto test TYPE."
  (pcase type
    ('no-test nil)
    (_ (ai-code--auto-test-inline-suffix-for-type type))))

(defun ai-code--ensure-auto-test-harness-file (type)
  "Return the package prompt file for TYPE, generating it when needed."
  (let* ((directory (ai-code--auto-test-harness-directory))
         (file-path (expand-file-name
                     (ai-code--auto-test-harness-file-name type)
                     directory)))
    (if (file-exists-p file-path)
        file-path
      (when-let ((content (ai-code--auto-test-harness-text-for-type type)))
        (ai-code--ensure-auto-test-harness-prompt-directory)
        (with-temp-file file-path
          (insert content)
          (unless (bolp)
            (insert "\n")))
        file-path))))

(defun ai-code--auto-test-harness-reference-suffix (type)
  "Return a short suffix that references the package prompt file for TYPE.

If the harness file cannot be prepared, fall back to the inline suffix."
  (condition-case err
      (when-let ((file-path (ai-code--ensure-auto-test-harness-file type)))
        (format
         "Read the local harness file: @%s. Use its instructions for this work. Apply it without repeating its full contents."
         (ai-code--auto-test-harness-prompt-path file-path)))
    (file-error
     (message "Failed to prepare auto-test harness file for %s: %s"
              type
              (error-message-string err))
     (ai-code--auto-test-inline-suffix-for-type type))))

(defun ai-code--auto-test-suffix-for-type (type)
  "Return prompt suffix for auto test TYPE."
  (pcase type
    ((or 'test-after-change 'tdd 'tdd-with-refactoring
         'uncle-bob-coding-agent-harness
         'uncle-bob-coding-agent-harness-plus)
     (ai-code--auto-test-harness-reference-suffix type))
    ('no-test "Do not write or run any test.")
    (_ nil)))

;;;; Grill Harness

;;;###autoload
(defcustom ai-code-grill-me-enabled nil
  "When non-nil, offer to clarify selected prompts before sending them.

The prompt is offered for commands in `ai-code--grill-me-commands' and
workflows in `ai-code--grill-me-workflows'.  When accepted, the request
tells the AI backend to read the bundled grilling harness before acting."
  :type 'boolean
  :group 'ai-code)

(defconst ai-code--grill-me-commands
  '(ai-code-code-change
    ai-code-ask-question
    ai-code-implement-todo
    ai-code-send-command
    ai-code-refactor-book-method)
  "Interactive commands that offer the grill-me harness.")

(defconst ai-code--grill-me-workflows
  '(investigate-issue review-pr resolve-merge-conflict)
  "Non-command workflows that offer the grill-me harness.")

(defvar ai-code--grill-me-workflow nil
  "Current non-command workflow considered by the Grill provider.")

(defconst ai-code--grill-me-context-cache-key 'ai-code-grill-me-accepted
  "Prompt context cache key for the current Grill decision.")

(defun ai-code--grill-me-harness-file ()
  "Return the bundled grill-me harness file path."
  (expand-file-name "grilling.v1.md"
                    (ai-code--auto-test-harness-directory)))

(defun ai-code--grill-me-reference-suffix ()
  "Return a short prompt suffix referencing the grill-me harness."
  (let ((file-path (ai-code--grill-me-harness-file)))
    (unless (file-readable-p file-path)
      (user-error "Grill-me harness is not readable: %s" file-path))
    (format
     "Read the local harness file: @%s. Use its instructions for this request. Apply them without repeating their full contents."
     (ai-code--auto-test-harness-prompt-path file-path))))

(defun ai-code--with-grill-me-origin (orig-fun &rest args)
  "Call ORIG-FUN with ARGS while preserving the entry command."
  (let ((ai-code--prompt-origin-command
         (or ai-code--prompt-origin-command this-command)))
    (apply orig-fun args)))

(defun ai-code--grill-me-accepted-p (context)
  "Return non-nil when Grill was accepted for prompt CONTEXT."
  (gethash ai-code--grill-me-context-cache-key
           (ai-code-prompt-context-cache context)))

(defun ai-code--grill-me-eligible-p (context)
  "Return non-nil when prompt CONTEXT should offer the Grill harness."
  (or (memq (ai-code-prompt-context-origin-command context)
            ai-code--grill-me-commands)
      (memq ai-code--grill-me-workflow ai-code--grill-me-workflows)))

(defun ai-code--grill-me-suffix-provider (context)
  "Return the optional Grill suffix for prompt CONTEXT."
  (let ((accepted
         (and ai-code-grill-me-enabled
              (ai-code--grill-me-eligible-p context)
              (y-or-n-p "Grill me before acting? "))))
    (puthash ai-code--grill-me-context-cache-key accepted
             (ai-code-prompt-context-cache context))
    (when accepted
      (ai-code--grill-me-reference-suffix))))

(add-hook 'ai-code-prompt-suffix-functions
          #'ai-code--grill-me-suffix-provider 20)

;; Remove the prompt-transform advice installed by pre-provider releases.
(when (advice-member-p 'ai-code--with-optional-grill-me
                       'ai-code--insert-prompt)
  (advice-remove 'ai-code--insert-prompt
                 'ai-code--with-optional-grill-me))

(defun ai-code--install-grill-me-command-advice ()
  "Install origin-preserving advice on available Grill commands."
  (dolist (command ai-code--grill-me-commands)
    (when (and (fboundp command)
               (not (advice-member-p #'ai-code--with-grill-me-origin command)))
      (advice-add command :around #'ai-code--with-grill-me-origin))))

(ai-code--install-grill-me-command-advice)

(dolist (feature '(ai-code-change ai-code-discussion ai-code))
  (with-eval-after-load feature
    (ai-code--install-grill-me-command-advice)))

;;;; Send-Time Routing: State and User Settings

(defvar ai-code-auto-test-suffix ai-code-test-after-code-change-suffix
  "Default prompt suffix to request running tests after code changes.")

(defvar ai-code-auto-test-type nil
  "Forward declaration for `ai-code-auto-test-type'.
See the later `defcustom' for user-facing documentation and default.")

(defvar ai-code-discussion-auto-follow-up-suffix nil
  "Send-time prompt suffix that requests numbered next-step suggestions.")

(defvar ai-code-discussion-auto-follow-up-enabled t
  "Forward declaration for `ai-code-discussion-auto-follow-up-enabled'.
See the later `defcustom' for user-facing documentation and default.")

(defvar ai-code-discussion-auto-follow-up-on-code-change nil
  "Forward declaration for `ai-code-discussion-auto-follow-up-on-code-change'.
See the later `defcustom' for user-facing documentation and default.")

(defconst ai-code--auto-test-type-ask-choices
  '(("Run tests after code change" . test-after-change)
    ("Do not write or run tests" . no-test)
    ("TDD Red + Green (write failing test, then make it pass)" . tdd)
    ("TDD Red + Green + Blue (refactor after Green)" . tdd-with-refactoring)
    ("Uncle Bob's coding agent harness" . uncle-bob-coding-agent-harness)
    ("Uncle Bob's coding agent harness+"
     . uncle-bob-coding-agent-harness-plus))
  "Resolve auto test suffix choices for `ask-me` mode.")

(defconst ai-code--auto-test-type-persistent-choices
  '(("Ask every time" . ask-me)
    ("Off" . nil))
  "Persistent choices for `ai-code-auto-test-type`.")

(defconst ai-code--auto-test-type-legacy-persistent-modes
  '(test-after-change tdd tdd-with-refactoring)
  "Legacy persistent values still honored for backward compatibility.")

(defun ai-code--read-auto-test-type-choice ()
  "Read and return one prompt test type for this send action."
  (let* ((choice (completing-read "Choose test prompt type for this send: "
                                  (mapcar #'car ai-code--auto-test-type-ask-choices)
                                  nil t nil nil
                                  (caar ai-code--auto-test-type-ask-choices)))
         (choice-cell (assoc choice ai-code--auto-test-type-ask-choices)))
    (if choice-cell
        (cdr choice-cell)
      'test-after-change)))

(defun ai-code--read-auto-follow-up-choice ()
  "Read whether to request numbered next-step suggestions for this send action."
  (y-or-n-p "Discussion follow-up suggestions? "))

;;;###autoload
(defcustom ai-code-use-gptel-classify-prompt nil
  "Whether to use GPTel to classify prompts before send-time suffix routing.
When non-nil and `ai-code-auto-test-type` or
`ai-code-discussion-auto-follow-up-enabled` is non-nil, classify whether
the current prompt is about code changes.  This lets code-change prompts
skip discussion follow-up suggestions, and discussion prompts skip auto
test suffixes."
  :type 'boolean
  :group 'ai-code)

;;;###autoload
(defcustom ai-code-next-step-suggestion-suffix
  (concat
   "At the end of your response, provide 3-4 numbered candidate next\n"
   "steps. Keep each option to one sentence. At least 2 candidates must\n"
   "be AI-actionable items as follow up: either a code change or tool usage. Mark the\n"
   "single best option with \"(Recommended)\". If the user replies with\n"
   "only a number such as 1, 2, 3, or 4, treat that as selecting the\n"
   "corresponding next step from your previous answer. The user may also\n"
   "ignore these options and send a different follow-up request instead.")
  "Prompt suffix for numbered next-step suggestions in discussion prompts."
  :type '(choice (const nil) string)
  :group 'ai-code)

;;;; Send-Time Routing: Prompt Classification

(defun ai-code--downcase-strings (strings)
  "Return STRINGS converted to lowercase."
  (mapcar #'downcase strings))

(defconst ai-code--code-change-prompt-markers
  (ai-code--downcase-strings
   (list ai-code-change--selected-region-note
         ai-code-change--generic-note
         ai-code-change--selected-files-note))
  "Prompt markers that clearly indicate a code-change request.")

(defconst ai-code--non-code-change-prompt-markers
  (append
   (ai-code--downcase-strings
    (list ai-code-change--ask-question-note
          ai-code-change--question-brief-default-boundaries
          ai-code-discussion--question-only-note
          ai-code-discussion--selected-region-note
          ai-code-discussion--exception-investigation-boundaries))
   (ai-code--downcase-strings
    ai-code-discussion--explain-prompt-prefixes)
   (ai-code--downcase-strings
    ai-code-github--analysis-only-notes))
  "Prompt markers that clearly indicate a non-code-change request.")

(defun ai-code--prompt-contains-any-marker-p (text markers)
  "Return non-nil when any string in MARKERS appears in TEXT."
  (seq-some (lambda (marker)
              (string-match-p (regexp-quote marker) text))
            markers))

(defun ai-code--simple-classify-prompt-code-change (prompt-text)
  "Classify PROMPT-TEXT with cheap string matching before GPTel.
Classification is driven only by PROMPT-TEXT, so editing a generated
analysis-only prompt into a code-change request reclassifies it.
Return one of: `code-change`, `non-code-change`, or `unknown`."
  (let ((text (downcase (or prompt-text ""))))
    (cond
     ((ai-code--prompt-contains-any-marker-p text
                                             ai-code--code-change-prompt-markers)
      'code-change)
     ((ai-code--prompt-contains-any-marker-p
       text ai-code--non-code-change-prompt-markers)
      'non-code-change)
     (t 'unknown))))

(defun ai-code--classify-prompt-code-change (prompt-text)
  "Classify whether PROMPT-TEXT requests a code change.
Use simple string matching first, then optionally fall back to GPTel."
  (let ((classification
         (ai-code--simple-classify-prompt-code-change prompt-text)))
    (if (and (eq classification 'unknown)
             ai-code-use-gptel-classify-prompt)
        (ai-code--gptel-classify-prompt-code-change prompt-text)
      classification)))

(defun ai-code--gptel-classify-prompt-code-change (prompt-text)
  "Classify whether PROMPT-TEXT requests a code change using GPTel.
Return one of: `code-change`, `non-code-change`, or `unknown`."
  (let ((classification
         (condition-case err
             (if (require 'gptel nil t)
                 (let* ((raw-answer (ai-code-call-gptel-sync
                                     (concat "Classify whether this user prompt requests program code changes in a repository.\n"
                                             "Reply with exactly one token: CODE_CHANGE or NOT_CODE_CHANGE.\n"
                                             "Focus only on the action explicitly requested; ignore code or files supplied as context.\n"
                                             "Return CODE_CHANGE only for changes to program code or test code.\n"
                                             "Only return CODE_CHANGE when the prompt explicitly asks to create, modify, fix, refactor, or delete program code or test code.\n"
                                             "Do not infer a code change merely because the prompt mentions or includes code, repository files, bugs, or implementation details.\n"
                                             "Treat documentation changes and any other non-program-code actions as NOT_CODE_CHANGE.\n"
                                             "Treat explain/summarize/discuss/review without editing as NOT_CODE_CHANGE.\n\n"
                                             "Prompt:\n" prompt-text)))
                        (answer (upcase (string-trim (or raw-answer "")))))
                   (pcase answer
                     ("CODE_CHANGE" 'code-change)
                     ("NOT_CODE_CHANGE" 'non-code-change)
                     (_ 'unknown)))
               'unknown)
           (error
            (message "GPTel prompt classification failed: %s" (error-message-string err))
            'unknown))))
    (message "GPTel prompt classification result: %s" classification)
    classification))

;;;; Send-Time Routing: Suffix Resolution

(defun ai-code--resolve-auto-test-type-for-send (&optional prompt-text classification)
  "Resolve the concrete auto test type for current send action for PROMPT-TEXT.
CLASSIFICATION is the optional prompt classification result."
  (if (eq ai-code-auto-test-type 'ask-me)
      (ai-code--resolve-ask-auto-test-type-for-send prompt-text classification)
    (and (memq ai-code-auto-test-type
               ai-code--auto-test-type-legacy-persistent-modes)
         ai-code-auto-test-type)))

(defun ai-code--resolve-ask-auto-test-type-for-send (&optional prompt-text classification)
  "Resolve the send-time auto test type for ask-me mode with PROMPT-TEXT.
CLASSIFICATION is the optional prompt classification result."
  (pcase (or classification
             (ai-code--classify-prompt-code-change prompt-text))
    ('code-change (ai-code--read-auto-test-type-choice))
    ('non-code-change nil)
    (_ (ai-code--read-auto-test-type-choice))))

(defun ai-code--ensure-discussion-follow-up-harness-file ()
  "Write and return the package prompt file path for discussion follow-up."
  (when-let ((content ai-code-next-step-suggestion-suffix))
    (let* ((directory (ai-code--ensure-auto-test-harness-prompt-directory))
           (file-path (expand-file-name "discussion-follow-up.v1.md" directory)))
      (unless (file-exists-p file-path)
        (with-temp-file file-path
          (insert content)
          (unless (bolp)
            (insert "\n"))))
      file-path)))

(defun ai-code--discussion-follow-up-reference-suffix ()
  "Return a short suffix that references the discussion follow-up prompt file.
If the harness file cannot be prepared, fall back to the inline suffix."
  (condition-case err
      (when-let ((file-path (ai-code--ensure-discussion-follow-up-harness-file)))
        (format
         "Read the local harness file: @%s. Use its instructions for this work. Apply it without repeating its full contents."
         (ai-code--auto-test-harness-prompt-path file-path)))
    (file-error
     (message "Failed to prepare discussion follow-up harness file: %s"
              (error-message-string err))
     ai-code-next-step-suggestion-suffix)))

(defun ai-code--resolve-auto-follow-up-suffix-for-send (&optional prompt-text classification)
  "Resolve next-step suggestion suffix for current send action for PROMPT-TEXT.
CLASSIFICATION is the optional prompt classification result."
  (when (and ai-code-discussion-auto-follow-up-enabled
             ai-code-next-step-suggestion-suffix)
    (let ((classification (or classification
                              (ai-code--classify-prompt-code-change
                               prompt-text))))
      (unless (and (eq classification 'code-change)
                   (not ai-code-discussion-auto-follow-up-on-code-change))
        (and (pcase ai-code-discussion-auto-follow-up-enabled
               ('always t)
               ((or 't 'ask-me) (ai-code--read-auto-follow-up-choice))
               (_ nil))
             (ai-code--discussion-follow-up-reference-suffix))))))

(defun ai-code--resolve-auto-test-suffix-for-send (&optional prompt-text classification)
  "Resolve auto test suffix for current send action for PROMPT-TEXT.
CLASSIFICATION is the optional prompt classification result."
  (ai-code--auto-test-suffix-for-type
   (ai-code--resolve-auto-test-type-for-send prompt-text classification)))

(defun ai-code--classify-prompt-for-send (&optional prompt-text)
  "Return prompt classification for PROMPT-TEXT when needed.
Send-time routing uses this result for test and discussion follow-up suffixes."
  (when (or ai-code-auto-test-type
            ai-code-discussion-auto-follow-up-enabled)
    (ai-code--classify-prompt-code-change prompt-text)))

;;;; Send-Time Routing: Providers and Setters

(defun ai-code--prompt-context-classification (context)
  "Return the shared code-change classification for prompt CONTEXT."
  (ai-code-prompt-context-memoize
   context
   'code-change-classification
   (lambda ()
     (ai-code--classify-prompt-for-send
      (ai-code-prompt-context-prompt-text context)))))

(defun ai-code--resolve-suffix-for-context (context resolver)
  "Call RESOLVER with the prompt and shared classification from CONTEXT."
  (funcall resolver
           (ai-code-prompt-context-prompt-text context)
           (ai-code--prompt-context-classification context)))

(defun ai-code--auto-test-suffix-provider (context)
  "Return the send-time auto-test suffix for prompt CONTEXT."
  (when (and (bound-and-true-p ai-code-use-prompt-suffix)
             ai-code-auto-test-type)
    (ai-code--resolve-suffix-for-context
     context #'ai-code--resolve-auto-test-suffix-for-send)))

(defun ai-code--discussion-follow-up-suffix-provider (context)
  "Return the send-time discussion follow-up suffix for prompt CONTEXT."
  (when (and (bound-and-true-p ai-code-use-prompt-suffix)
             ai-code-discussion-auto-follow-up-enabled
             (not (ai-code--grill-me-accepted-p context)))
    (ai-code--resolve-suffix-for-context
     context #'ai-code--resolve-auto-follow-up-suffix-for-send)))

(add-hook 'ai-code-prompt-suffix-functions
          #'ai-code--auto-test-suffix-provider 30)
(add-hook 'ai-code-prompt-suffix-functions
          #'ai-code--discussion-follow-up-suffix-provider 40)

;; Remove the send-time advice installed by pre-provider releases.
(when (advice-member-p 'ai-code--with-auto-test-suffix-for-send
                       'ai-code--write-prompt-to-file-and-send)
  (advice-remove 'ai-code--write-prompt-to-file-and-send
                 'ai-code--with-auto-test-suffix-for-send))

(defun ai-code--test-after-code-change--set (symbol value)
  "Set SYMBOL to VALUE and update related suffix behavior."
  (set-default symbol value)
  (set symbol value)
  (setq ai-code-auto-test-suffix
        (ai-code--auto-test-suffix-for-type value)))

(defun ai-code--apply-auto-test-type (value)
  "Set `ai-code-auto-test-type` to VALUE and refresh related suffix."
  (setq ai-code-auto-test-type value)
  (ai-code--test-after-code-change--set 'ai-code-auto-test-type value)
  value)

(defun ai-code--cycle-auto-test-type-value (current-val)
  "Return the next cycled value of `ai-code-auto-test-type` for CURRENT-VAL."
  (if (eq current-val 'ask-me)
      nil
    'ask-me))

(defun ai-code--apply-discussion-auto-follow-up-enabled (value)
  "Set `ai-code-discussion-auto-follow-up-enabled` to VALUE."
  (setq ai-code-discussion-auto-follow-up-enabled value)
  value)

(defun ai-code--cycle-discussion-auto-follow-up-value (current-val)
  "Return the next cycled auto-follow-up-enabled setting for CURRENT-VAL."
  (pcase current-val
    ('nil 'ask-me)
    ((or 't 'ask-me) 'always)
    ('always 'nil)
    (_ 'ask-me)))

(defcustom ai-code-auto-test-type nil
  "Select how prompts request tests after code changes."
  :type '(choice (const :tag "Ask every time" ask-me)
                 (const :tag "Off" nil))
  :set #'ai-code--test-after-code-change--set
  :group 'ai-code)

;;;###autoload
(defcustom ai-code-discussion-auto-follow-up-on-code-change nil
  "Whether to allow discussion follow-up suggestions for code-change prompts.
When non-nil, next-step suggestions can be appended to code-change prompts
as well, depending on the routing choice."
  :type 'boolean
  :group 'ai-code)

(defcustom ai-code-discussion-auto-follow-up-enabled t
  "When non-nil, prompts may request numbered next-step suggestions.
This can be nil to disable it, always to always append without prompt,
or ask-me (or t) to ask the user on each send."
  :type '(choice (const :tag "Ask each send" ask-me)
                 (const :tag "Always" always)
                 (const :tag "Off" nil))
  :set (lambda (symbol value)
         (set-default symbol value)
         (set symbol value))
  :group 'ai-code)

(provide 'ai-code-harness)

;;; ai-code-harness.el ends here
