use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{GetPromptResult, PromptMessage, Role};
use rmcp::{prompt, prompt_router};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use super::SymForgeServer;
use crate::protocol::resources::{
    file_context_resource, repo_changes_resource, repo_health_resource, repo_map_resource,
    repo_outline_resource, tools_catalog_resource,
};

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct CodeReviewPromptInput {
    pub path: Option<String>,
    pub focus: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct ArchitectureMapPromptInput {
    pub area: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct FailureTriagePromptInput {
    pub symptom: String,
    pub path: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct OnboardPromptInput {
    /// Optional area or module to focus on first.
    pub area: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct RefactorPromptInput {
    /// What you want to refactor (e.g., "rename ErrorKind to AppError", "extract validation logic").
    pub goal: String,
    /// Optional file or symbol to start from.
    pub target: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DebugPromptInput {
    /// The error message, stack trace, or unexpected behavior.
    pub error: String,
    /// Optional file path where the error occurs.
    pub path: Option<String>,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
pub struct AdminPromptInput {}

#[prompt_router(vis = "pub(crate)")]
impl SymForgeServer {
    #[prompt(
        name = "symforge-review",
        description = "Generate a code review plan using SymForge context surfaces."
    )]
    pub(crate) async fn code_review_prompt(
        &self,
        params: Parameters<CodeReviewPromptInput>,
    ) -> GetPromptResult {
        let mut messages = vec![
            PromptMessage::new_text(
                Role::User,
                build_code_review_instructions(&self.project_name, &params.0),
            ),
            PromptMessage::new_resource_link(Role::User, repo_health_resource()),
            PromptMessage::new_resource_link(Role::User, repo_map_resource()),
        ];

        if let Some(path) = params.0.path.as_deref() {
            messages.push(PromptMessage::new_resource_link(
                Role::User,
                file_context_resource(path, Some(200)),
            ));
        }

        GetPromptResult::new(messages)
            .with_description("Review code using SymForge resources and targeted tools.")
    }

    #[prompt(
        name = "symforge-architecture",
        description = "Generate an architecture mapping plan using SymForge repo context."
    )]
    pub(crate) async fn architecture_map_prompt(
        &self,
        params: Parameters<ArchitectureMapPromptInput>,
    ) -> GetPromptResult {
        let mut messages = vec![
            PromptMessage::new_text(
                Role::User,
                build_architecture_map_instructions(&self.project_name, params.0.area.as_deref()),
            ),
            PromptMessage::new_resource_link(Role::User, repo_map_resource()),
            PromptMessage::new_resource_link(Role::User, repo_outline_resource()),
            PromptMessage::new_resource_link(Role::User, repo_health_resource()),
        ];

        if let Some(area) = params.0.area.as_deref() {
            messages.push(PromptMessage::new_text(
                Role::User,
                format!("Prioritize the area or subsystem named '{area}' if it exists."),
            ));
        }

        GetPromptResult::new(messages).with_description(
            "Map repository architecture using SymForge resources and cross-reference tools.",
        )
    }

    #[prompt(
        name = "symforge-triage",
        description = "Generate a debugging and failure-triage plan using SymForge state."
    )]
    pub(crate) async fn failure_triage_prompt(
        &self,
        params: Parameters<FailureTriagePromptInput>,
    ) -> GetPromptResult {
        let mut messages = vec![
            PromptMessage::new_text(
                Role::User,
                build_failure_triage_instructions(&self.project_name, &params.0),
            ),
            PromptMessage::new_resource_link(Role::User, repo_health_resource()),
            PromptMessage::new_resource_link(Role::User, repo_changes_resource()),
            PromptMessage::new_resource_link(Role::User, repo_map_resource()),
        ];

        if let Some(path) = params.0.path.as_deref() {
            messages.push(PromptMessage::new_resource_link(
                Role::User,
                file_context_resource(path, Some(200)),
            ));
        }

        GetPromptResult::new(messages).with_description(
            "Triage failures using SymForge runtime health, changed files, and local context.",
        )
    }

    #[prompt(
        name = "symforge-onboard",
        description = "Generate a codebase onboarding plan using SymForge for guided exploration."
    )]
    pub(crate) async fn onboard_prompt(
        &self,
        params: Parameters<OnboardPromptInput>,
    ) -> GetPromptResult {
        let mut messages = vec![
            PromptMessage::new_text(
                Role::User,
                build_onboard_instructions(&self.project_name, params.0.area.as_deref()),
            ),
            PromptMessage::new_resource_link(Role::User, repo_map_resource()),
            PromptMessage::new_resource_link(Role::User, repo_outline_resource()),
            PromptMessage::new_resource_link(Role::User, repo_health_resource()),
            PromptMessage::new_resource_link(Role::User, tools_catalog_resource()),
        ];

        if let Some(area) = params.0.area.as_deref() {
            messages.push(PromptMessage::new_text(
                Role::User,
                format!("Focus onboarding on the '{area}' area first."),
            ));
        }

        GetPromptResult::new(messages)
            .with_description("Onboard to a codebase using layered SymForge exploration.")
    }

    #[prompt(
        name = "symforge-refactor",
        description = "Generate a refactoring plan with impact analysis using SymForge."
    )]
    pub(crate) async fn refactor_prompt(
        &self,
        params: Parameters<RefactorPromptInput>,
    ) -> GetPromptResult {
        let mut messages = vec![
            PromptMessage::new_text(
                Role::User,
                build_refactor_instructions(&self.project_name, &params.0),
            ),
            PromptMessage::new_resource_link(Role::User, repo_map_resource()),
            PromptMessage::new_resource_link(Role::User, repo_health_resource()),
        ];

        if let Some(target) = params.0.target.as_deref() {
            messages.push(PromptMessage::new_resource_link(
                Role::User,
                file_context_resource(target, Some(200)),
            ));
        }

        GetPromptResult::new(messages)
            .with_description("Plan a refactoring with full impact analysis using SymForge.")
    }

    #[prompt(
        name = "symforge-debug",
        description = "Generate a detailed debugging plan using SymForge call tracing and change analysis."
    )]
    pub(crate) async fn debug_prompt(
        &self,
        params: Parameters<DebugPromptInput>,
    ) -> GetPromptResult {
        let mut messages = vec![
            PromptMessage::new_text(
                Role::User,
                build_debug_instructions(&self.project_name, &params.0),
            ),
            PromptMessage::new_resource_link(Role::User, repo_health_resource()),
            PromptMessage::new_resource_link(Role::User, repo_changes_resource()),
        ];

        if let Some(path) = params.0.path.as_deref() {
            messages.push(PromptMessage::new_resource_link(
                Role::User,
                file_context_resource(path, Some(200)),
            ));
        }

        GetPromptResult::new(messages)
            .with_description("Debug a problem using SymForge call tracing and impact analysis.")
    }

    #[prompt(
        name = "symforge-admin",
        description = "Return the running SymForge operator dashboard URL, or guidance to start it."
    )]
    pub(crate) async fn admin_prompt(
        &self,
        _params: Parameters<AdminPromptInput>,
    ) -> GetPromptResult {
        // The universal in-harness affordance (FR-016, Constitution II): every
        // MCP-speaking harness gets this, with or without a command file.
        //
        // Execution-model decision (009 US3): a prompt handler is a pure,
        // read-only fetch. It does NOT start a server — starting one is a
        // blocking, side-effectful process action that belongs to the
        // `symforge admin` CLI verb, and `operator_server_reachable` /
        // `start_operator_server` build their own current-thread tokio runtime and
        // `block_on`, which would panic if invoked directly on this async worker.
        // Instead the prompt does the read-only reachability check off the async
        // runtime (via `spawn_blocking`) and returns the reachable dashboard URL,
        // or guidance to run `symforge admin` when nothing is up yet.
        let repo_root = self.capture_repo_root();
        let dashboard_url =
            tokio::task::spawn_blocking(move || resolve_running_dashboard_url(repo_root))
                .await
                .unwrap_or(None);

        let body = build_admin_instructions(&self.project_name, dashboard_url.as_deref());
        GetPromptResult::new(vec![PromptMessage::new_text(Role::User, body)])
            .with_description(
                "Open the SymForge operator dashboard (reuse the running server, or start it via `symforge admin`).",
            )
    }
}

/// Read-only resolution of the running operator dashboard URL for the
/// `symforge-admin` prompt: load the project's `OperatorSetupProfile` for the
/// remembered port and, if a server is reachable there, return its `/admin` URL.
/// Returns `None` when there is no profile or nothing serves the remembered port.
///
/// This performs an HTTP reachability probe on its own current-thread runtime, so
/// it MUST run off the async worker (the caller wraps it in `spawn_blocking`). It
/// never starts a server — that is the `symforge admin` CLI verb's job (D3).
fn resolve_running_dashboard_url(repo_root: Option<std::path::PathBuf>) -> Option<String> {
    use std::time::Duration;
    let base = repo_root?;
    let profile = crate::cli::operator_profile::OperatorSetupProfile::load(&base)?;
    let addr = std::net::SocketAddr::new(
        std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
        profile.port,
    );
    if crate::cli::admin::operator_server_reachable(addr, Duration::from_millis(500)) {
        Some(format!("http://{addr}/admin"))
    } else {
        None
    }
}

fn build_admin_instructions(project_name: &str, dashboard_url: Option<&str>) -> String {
    match dashboard_url {
        Some(url) => format!(
            "## SymForge Operator Dashboard for '{project_name}'\n\
             \n\
             The operator dashboard is running. Open it here:\n\
             \n\
             {url}\n\
             \n\
             This is the live SymForge admin view (index health, harness attach status, \
             change activity). If the link does not open in your environment, copy the URL \
             into a browser."
        ),
        None => format!(
            "## SymForge Operator Dashboard for '{project_name}'\n\
             \n\
             No operator dashboard is currently running for this project.\n\
             \n\
             Start it by running the CLI verb in a terminal at the project root:\n\
             \n\
             ```\n\
             symforge admin\n\
             ```\n\
             \n\
             `symforge admin` prints the dashboard URL. If it STARTS a new server (nothing \
             was running on the remembered port) it stays in the foreground and keeps serving \
             until you stop it (Ctrl-C); if it REUSES a server already running, it returns \
             immediately (the dashboard stays up because another process owns it). This prompt \
             only reports an already-running dashboard — it does not start the server itself."
        ),
    }
}

/// Shared surface-mapping preamble for the six tool-driven prompt bodies. The
/// step tool names below are full-surface spellings (`SYMFORGE_SURFACE=full`);
/// on the default compact surface the agent has only
/// `symforge`/`symforge_edit`/`status`, so the mapping is stated once here
/// instead of dual-spelling every step. `build_admin_instructions` is excluded:
/// it drives the `symforge admin` CLI verb, not the MCP tool surface.
const SURFACE_NOTE: &str = "> Surface note: the tool names in these steps are full-surface \
spellings (`SYMFORGE_SURFACE=full`). On the default compact surface you have three tools — \
`symforge` (read/explore: pass a natural-language `query` such as \"find symbol X\", \
\"who calls X\", or \"what changed\", optionally with an `intent` hint of \
orient/find/read/trace/impact/meta/auto), `symforge_edit` (structural edits), and \
`status` (which names your active surface). When on compact, route each step below through \
`symforge`/`symforge_edit`.\n";

fn build_code_review_instructions(project_name: &str, input: &CodeReviewPromptInput) -> String {
    let target = input.path.as_deref().map_or(
        "Start from `what_changed(uncommitted=true, code_only=true)` to find all modified files \
         (the path list caps at 200 — narrow with `path_prefix` if it truncates)."
            .to_string(),
        |p| format!("Start with the target path '{p}'."),
    );
    let focus = input.focus.as_deref().map_or(String::new(), |f| {
        format!("\n\nPay special attention to: {f}.")
    });

    format!(
        "## Code Review Workflow for '{project_name}'\n\
         \n\
         {SURFACE_NOTE}\
         \n\
         ### Step 1: Scope the Review\n\
         {target}\n\
         - Call `diff_symbols(code_only=true)` to see which symbols changed\n\
         - Call `detect_impact(scope=\"symbols\")` for the change's blast radius vs the base branch (origin/main by default; each list caps at 200 with {{total,returned,truncated}} pagination). Surface caveat: through the compact `symforge` facade `intent=impact` yields the file-scoped impact only — `scope=\"symbols\"` needs the full surface\n\
         - If > 20 symbols changed, use `diff_symbols(compact=true)` first for overview\n\
         \n\
         ### Step 2: Prioritize by Risk\n\
         For each changed symbol:\n\
         - Call `find_references(name=\"<sym>\", compact=true)` — symbols with >5 callers are HIGH RISK\n\
         - Call `get_symbol_context(name=\"<sym>\", verbosity=\"signature\")` — check for signature changes\n\
         \n\
         ### Step 3: Deep Review (high-risk symbols only)\n\
         - Call `get_symbol_context(name=\"<sym>\", bundle=true)` to see the symbol + all type deps\n\
         - Check: Are all callers still compatible with the change?\n\
         - Check: Did any type dependency change shape?\n\
         - Check: Are there missing error-handling paths?\n\
         \n\
         ### Step 4: Check for Missing Tests\n\
         - For each modified symbol, search for corresponding test functions\n\
         - Call `search_symbols(query=\"test_<sym_name>\", include_tests=true)`\n\
         - Flag any changed symbols without test coverage\n\
         \n\
         ### Step 5: Report\n\
         Summarize: what changed, risk assessment per symbol, broken contracts, missing tests.{focus}"
    )
}

fn build_architecture_map_instructions(project_name: &str, area: Option<&str>) -> String {
    let area_note = area.map_or(String::new(), |a| {
        format!("\n\nFocus area: '{a}'. Prioritize this subsystem and its connections.")
    });

    format!(
        "## Architecture Mapping Workflow for '{project_name}'\n\
         \n\
         {SURFACE_NOTE}\
         \n\
         ### Step 1: Get the Big Picture\n\
         - Read the repo map resource (attached) for directory structure and key types\n\
         - Doctrine: the map orients; the tools prove. Treat the repo map as a ranked starting point, not an inventory.\n\
         - Absence from the map is not absence from the repo - confirm with `search_symbols` / `search_text` before concluding something is missing.\n\
         - Call `get_repo_map(detail=\"tree\", depth=2)` to see the file tree with symbol counts\n\
         - Identify the top-level modules/crates/packages\n\
         \n\
         ### Step 2: Map Subsystem Boundaries\n\
         For each major directory:\n\
         - Call `get_file_context(path=\"<dir>/mod.rs\", sections=[\"outline\",\"imports\"])` (or equivalent entry file)\n\
         - Note: which modules import which? What are the public exports?\n\
         - Call `find_dependents(path=\"<key_file>\", compact=true)` for core files\n\
         \n\
         ### Step 3: Identify the Core Types\n\
         - Call `search_symbols(kind=\"struct\", limit=20)` to find the main data structures\n\
         - For each core struct: `get_symbol_context(name=\"<struct>\", verbosity=\"signature\")` to see its shape\n\
         - Call `find_references(name=\"<struct>\", mode=\"implementations\")` to find trait impls\n\
         \n\
         ### Step 4: Trace Data Flow\n\
         - Pick 2-3 key entry points (main, handlers, API routes)\n\
         - Call `get_symbol_context(name=\"<entry>\", sections=[])` for full trace: callers → callees → types\n\
         - Follow the call chain 2-3 levels deep to map the hot path\n\
         \n\
         ### Step 5: Report\n\
         Produce: subsystem diagram, ownership boundaries, key data flows, and which files/symbols to investigate next.{area_note}"
    )
}

fn build_failure_triage_instructions(
    project_name: &str,
    input: &FailureTriagePromptInput,
) -> String {
    let hotspot = input.path.as_deref().map_or(String::new(), |p| {
        format!("\n\nInitial hotspot: '{p}'. Start investigation here.")
    });

    format!(
        "## Failure Triage Workflow for '{project_name}'\n\
         Symptom: {symptom}\n\
         \n\
         {SURFACE_NOTE}\
         \n\
         ### Step 1: Check Recent Changes\n\
         - Call `what_changed(uncommitted=true, code_only=true, include_symbol_diff=true)` (path list caps at 200 — narrow with `path_prefix` if it truncates)\n\
         - If the symptom appeared recently, the bug is likely in uncommitted changes\n\
         - For a suspected regression, `detect_impact` shows what changed vs the base branch (origin/main by default) and its blast radius\n\
         - Check `diff_symbols()` — which functions were modified?\n\
         \n\
         ### Step 2: Locate the Failure Point\n\
         - Extract key terms from the symptom (error message, function name, file path)\n\
         - Call `search_text(query=\"<error_text>\")` to find where the error originates\n\
         - Call `inspect_match(path=\"<file>\", line=<N>)` to see the enclosing symbol and context\n\
         \n\
         ### Step 3: Trace the Call Chain\n\
         - Call `get_symbol_context(name=\"<failing_fn>\", sections=[])` for full trace\n\
         - Follow callers: who invokes this function? What inputs does it receive?\n\
         - Follow callees: what dependencies could be causing the failure?\n\
         \n\
         ### Step 4: Check Type Contracts\n\
         - Call `get_symbol_context(name=\"<failing_fn>\", bundle=true)` to see all type deps\n\
         - Did any struct/enum change shape recently? (compare with diff_symbols output)\n\
         - Are there None/null paths not handled?\n\
         \n\
         ### Step 5: Narrow to Root Cause\n\
         - If in changed code: the diff is the likely root cause\n\
         - If in unchanged code: look for dependency changes or environment issues\n\
         - Call `search_text(query=\"<suspect_pattern>\", follow_refs=true)` to check impact\n\
         \n\
         ### Step 6: Report\n\
         Root cause, affected scope (how many callers), suggested fix, and verification steps.{hotspot}",
        symptom = input.symptom
    )
}

fn build_onboard_instructions(project_name: &str, area: Option<&str>) -> String {
    let area_note = area.map_or(String::new(), |a| {
        format!("\n\nStart with the '{a}' area before expanding to the rest.")
    });

    format!(
        "## Codebase Onboarding Workflow for '{project_name}'\n\
         \n\
         {SURFACE_NOTE}\
         \n\
         ### Step 1: Project Overview (2 minutes)\n\
         - Read the repo map resource (attached) for structure and languages\n\
         - Doctrine: the map orients; the tools prove. Treat the repo map as a ranked starting point, not an inventory.\n\
         - Absence from the map is not absence from the repo - confirm with `search_symbols` / `search_text` before concluding something is missing.\n\
         - Call `get_repo_map(detail=\"tree\", depth=2)` to see directory layout\n\
         - Identify: What language? How many modules? What's the entry point?\n\
         \n\
         ### Step 2: Understand the Architecture (5 minutes)\n\
         - Call `explore(query=\"main entry point\", depth=2)` to find the main function/handler\n\
         - For each top-level directory, call `get_file_context(path=\"<dir>/mod.rs\", sections=[\"outline\"])`\n\
         - Map: what are the 3-5 core modules and what does each do?\n\
         \n\
         ### Step 3: Find the Core Types (3 minutes)\n\
         - Call `search_symbols(kind=\"struct\", limit=15)` to find main data structures\n\
         - Call `search_symbols(kind=\"trait\", limit=10)` to find key abstractions\n\
         - For the top 3 types: `get_symbol(name=\"<type>\")` to read their definition\n\
         \n\
         ### Step 4: Trace a Key Flow (5 minutes)\n\
         - Pick the most important entry point (main, handle_request, process, etc.)\n\
         - Call `get_symbol_context(name=\"<entry>\", sections=[\"dependents\",\"siblings\"])` for full trace\n\
         - Follow 2-3 levels of callees to understand the hot path\n\
         \n\
         ### Step 5: Check Test Patterns (2 minutes)\n\
         - Call `search_files(query=\"test\")` to find test files\n\
         - Call `get_file_context(path=\"<test_file>\", sections=[\"outline\"])` on one test file\n\
         - Note: how are tests organized? What patterns are used?\n\
         \n\
         ### Step 6: Summary\n\
         Produce a concise mental model: purpose, architecture, core types, data flow, test approach.{area_note}"
    )
}

fn build_refactor_instructions(project_name: &str, input: &RefactorPromptInput) -> String {
    let target_note = input
        .target
        .as_deref()
        .map_or(String::new(), |t| format!("\n\nStarting point: '{t}'."));

    format!(
        "## Refactoring Workflow for '{project_name}'\n\
         Goal: {goal}\n\
         \n\
         {SURFACE_NOTE}\
         \n\
         ### Step 1: Understand Current State\n\
         - Call `search_symbols(query=\"<target>\")` to find the symbol(s) involved\n\
         - Call `get_symbol_context(name=\"<sym>\", bundle=true)` to see the full definition + type deps\n\
         - Call `find_references(name=\"<sym>\", compact=true)` to count all usage sites\n\
         \n\
         ### Step 2: Assess Impact Radius\n\
         - How many files reference this symbol? (from find_references)\n\
         - Call `find_dependents(path=\"<file>\", compact=true)` to see file-level impact\n\
         - Are there >10 callers? If so, this is a high-risk refactor — plan carefully\n\
         \n\
         ### Step 3: Plan the Edit Sequence\n\
         Based on the refactor type:\n\
         - **Rename**: Use `batch_rename(dry_run=true)` to preview all changes\n\
         - **Extract**: Identify the code to extract with `get_symbol`, then `edit_within_symbol` + `insert_symbol`\n\
         - **Restructure**: Use `batch_edit(dry_run=true)` for multi-symbol changes\n\
         - **Delete**: Check `find_references` first — ensure zero callers before `delete_symbol`\n\
         \n\
         ### Step 4: Execute with dry_run First\n\
         - Always run with `dry_run=true` first to preview\n\
         - Review the preview for unintended changes\n\
         - Then execute without dry_run\n\
         \n\
         ### Step 5: Verify\n\
         - Call `analyze_file_impact(path=\"<changed_file>\")` for each modified file\n\
         - Call `detect_impact(scope=\"symbols\")` for the whole change's blast radius vs the base branch (origin/main by default; each list caps at 200 with {{total,returned,truncated}} pagination)\n\
         - Check for stale references in the impact report\n\
         - Search for any remaining old names: `search_text(query=\"<old_name>\")`{target_note}",
        goal = input.goal
    )
}

fn build_debug_instructions(project_name: &str, input: &DebugPromptInput) -> String {
    let path_note = input
        .path
        .as_deref()
        .map_or(String::new(), |p| format!("\n\nSuspected file: '{p}'."));

    format!(
        "## Debugging Workflow for '{project_name}'\n\
         Error: {error}\n\
         \n\
         {SURFACE_NOTE}\
         \n\
         ### Step 1: Find the Error Origin\n\
         - Extract the key error text or pattern from the error message\n\
         - Call `search_text(query=\"<error_pattern>\")` to find where it's generated\n\
         - If it's a function name: `search_symbols(query=\"<fn_name>\")`\n\
         - Call `inspect_match(path=\"<file>\", line=<N>)` on the match for full context\n\
         \n\
         ### Step 2: Understand the Failing Function\n\
         - Call `get_symbol(name=\"<fn>\")` to read the full body (`path` is optional — name alone resolves; add `path`/`symbol_line` only to disambiguate a shared name)\n\
         - Call `get_symbol_context(name=\"<fn>\", sections=[])` for callers + callees + types\n\
         - Map the data flow: what goes in, what comes out, what can fail?\n\
         \n\
         ### Step 3: Check Recent Changes (is this a regression?)\n\
         - Call `what_changed(uncommitted=true, include_symbol_diff=true)` (path list caps at 200 — narrow with `path_prefix` if it truncates)\n\
         - Call `detect_impact` to see what changed vs the base branch (origin/main by default) and whether the blast radius reaches the failing function\n\
         - Did the failing function change recently?\n\
         - Did any of its dependencies change? Check `diff_symbols()`\n\
         \n\
         ### Step 4: Check Callers\n\
         - Call `find_references(name=\"<fn>\", compact=true)` to find all call sites\n\
         - Are callers passing unexpected inputs? Check their code with `get_symbol`\n\
         - Look for pattern mismatches (wrong types, missing error handling)\n\
         \n\
         ### Step 5: Check Dependencies\n\
         - Call `get_symbol_context(name=\"<fn>\", bundle=true)` for all type deps\n\
         - Did any struct/enum change shape? Are there Option/Result paths not handled?\n\
         - Call `search_text(query=\"unwrap()\", glob=\"<suspect_file>\")` to find panic points\n\
         \n\
         ### Step 6: Root Cause Report\n\
         Root cause, evidence trail, affected scope, suggested fix, and how to verify.{path_note}",
        error = input.error
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use parking_lot::Mutex;
    use std::collections::HashMap;
    use std::sync::Arc;
    use std::time::{Duration, Instant};

    use crate::live_index::store::{CircuitBreakerState, LiveIndex};
    use crate::watcher::WatcherInfo;

    use crate::protocol::resources::REPO_HEALTH_URI;

    fn make_server() -> SymForgeServer {
        let index = LiveIndex {
            files: HashMap::new(),
            loaded_at: Instant::now(),
            loaded_at_system: std::time::SystemTime::now(),
            load_duration: Duration::from_millis(1),
            cb_state: CircuitBreakerState::new(0.20),
            is_empty: false,
            load_source: crate::live_index::store::IndexLoadSource::FreshLoad,
            snapshot_verify_state: crate::live_index::store::SnapshotVerifyState::NotNeeded,
            reverse_index: HashMap::new(),
            files_by_basename: HashMap::new(),
            files_by_dir_component: HashMap::new(),
            trigram_index: crate::live_index::trigram::TrigramIndex::new(),
            gitignore: None,
            skipped_files: Vec::new(),
            coupling_store: None,
            local_empty_reason: std::sync::Arc::new(parking_lot::RwLock::new(None)),
            indexed_root: None,
        };

        SymForgeServer::new(
            crate::live_index::SharedIndexHandle::shared(index),
            "prompt_project".to_string(),
            Arc::new(Mutex::new(WatcherInfo::default())),
            None,
            None,
        )
    }

    #[test]
    fn test_prompt_router_lists_expected_prompts() {
        let server = make_server();
        let prompts = server.prompt_router.list_all();
        let by_name: HashMap<&str, &rmcp::model::Prompt> =
            prompts.iter().map(|p| (p.name.as_str(), p)).collect();

        // All seven prompts are advertised. 009 US3 (T024): symforge-admin is the
        // universal in-harness affordance.
        for name in [
            "symforge-review",
            "symforge-architecture",
            "symforge-triage",
            "symforge-onboard",
            "symforge-refactor",
            "symforge-debug",
            "symforge-admin",
        ] {
            assert!(by_name.contains_key(name), "missing prompt: {name}");
        }

        // Regression guard for the prompts/list `description` each client renders
        // in its slash-command menu. A client once showed `symforge-architecture`
        // mangled ("using Synts: area)"); the SOURCE string was intact, so the
        // mangling was client-side truncation. This pins every source description
        // exactly, so a genuine source-side corruption would fail here.
        let expect_desc = |name: &str, desc: &str| {
            assert_eq!(
                by_name[name].description.as_deref(),
                Some(desc),
                "list description drift for {name}"
            );
        };
        expect_desc(
            "symforge-review",
            "Generate a code review plan using SymForge context surfaces.",
        );
        expect_desc(
            "symforge-architecture",
            "Generate an architecture mapping plan using SymForge repo context.",
        );
        expect_desc(
            "symforge-triage",
            "Generate a debugging and failure-triage plan using SymForge state.",
        );
        expect_desc(
            "symforge-onboard",
            "Generate a codebase onboarding plan using SymForge for guided exploration.",
        );
        expect_desc(
            "symforge-refactor",
            "Generate a refactoring plan with impact analysis using SymForge.",
        );
        expect_desc(
            "symforge-debug",
            "Generate a detailed debugging plan using SymForge call tracing and change analysis.",
        );
        expect_desc(
            "symforge-admin",
            "Return the running SymForge operator dashboard URL, or guidance to start it.",
        );

        // Declared arguments (prompts/list) must match the input structs,
        // including which are optional. rmcp derives `required` from the schema:
        // `Option<_>` fields are not required.
        let args = |name: &str| -> HashMap<String, bool> {
            by_name[name]
                .arguments
                .as_ref()
                .map(|list| {
                    list.iter()
                        .map(|a| (a.name.clone(), a.required.unwrap_or(false)))
                        .collect()
                })
                .unwrap_or_default()
        };
        assert_eq!(
            args("symforge-review"),
            HashMap::from([("path".into(), false), ("focus".into(), false)]),
        );
        assert_eq!(
            args("symforge-architecture"),
            HashMap::from([("area".into(), false)]),
        );
        assert_eq!(
            args("symforge-triage"),
            HashMap::from([("symptom".into(), true), ("path".into(), false)]),
        );
        assert_eq!(
            args("symforge-onboard"),
            HashMap::from([("area".into(), false)]),
        );
        assert_eq!(
            args("symforge-refactor"),
            HashMap::from([("goal".into(), true), ("target".into(), false)]),
        );
        assert_eq!(
            args("symforge-debug"),
            HashMap::from([("error".into(), true), ("path".into(), false)]),
        );
        // AdminPromptInput has no fields -> no declared arguments.
        assert!(args("symforge-admin").is_empty());
    }

    #[tokio::test]
    async fn test_admin_prompt_returns_guidance_when_no_server() {
        // make_server() has no bound repo_root, so there is no profile / running
        // server to resolve: the prompt must return the honest "run `symforge
        // admin`" guidance rather than a fabricated URL (URL-or-guidance, D2).
        let server = make_server();
        let result = server
            .admin_prompt(Parameters(AdminPromptInput::default()))
            .await;

        let text = result
            .messages
            .iter()
            .find_map(|message| match &message.content {
                rmcp::model::ContentBlock::Text(content) => Some(content.text.clone()),
                _ => None,
            })
            .expect("admin prompt returns a text message");

        // Guidance points at the CLI verb that actually starts the server.
        assert!(
            text.contains("symforge admin"),
            "no-server guidance must name the `symforge admin` verb: {text}"
        );
        assert!(
            text.contains("No operator dashboard is currently running"),
            "must honestly report nothing is running: {text}"
        );
        // D21: a fresh `symforge admin` stays in the foreground (does not fire-and-
        // return); the guidance must describe the foreground-serving behavior, while
        // the reuse path returns immediately.
        assert!(
            text.contains("stays in the foreground"),
            "guidance must describe foreground serving on fresh start (D21): {text}"
        );
        assert!(
            text.contains("returns immediately"),
            "guidance must state the reuse path returns immediately: {text}"
        );
    }

    #[test]
    fn test_admin_instructions_report_running_url_when_present() {
        // When a dashboard URL is resolved, the prompt body surfaces exactly that
        // URL (the reuse path) and does not fall back to guidance.
        let url = "http://127.0.0.1:8787/admin";
        let body = build_admin_instructions("prompt_project", Some(url));
        assert!(
            body.contains(url),
            "running body must include the URL: {body}"
        );
        assert!(
            body.contains("dashboard is running"),
            "running body must say the dashboard is up: {body}"
        );
    }

    #[tokio::test]
    async fn test_code_review_prompt_includes_resource_links() {
        let server = make_server();
        let result = server
            .code_review_prompt(Parameters(CodeReviewPromptInput {
                path: Some("src/lib.rs".to_string()),
                focus: Some("dependency risks".to_string()),
            }))
            .await;

        assert!(
            result.messages.iter().any(|message| matches!(
                &message.content,
                rmcp::model::ContentBlock::ResourceLink(link)
                    if link.uri == REPO_HEALTH_URI
            )),
            "symforge-review prompt should link repo health"
        );
        assert!(
            result.messages.iter().any(|message| matches!(
                &message.content,
                rmcp::model::ContentBlock::ResourceLink(link)
                    if link.uri.contains("symforge://file/context")
            )),
            "symforge-review prompt should link file context"
        );
    }

    #[test]
    fn test_onboard_instructions_embed_orientation_doctrine() {
        let body = build_onboard_instructions("prompt_project", None);
        // Statement 1: the map orients; the tools prove.
        assert!(
            body.contains("map orients"),
            "onboarding instructions must embed the 'map orients' doctrine: {body}"
        );
        // Statement 2: absence from the map is not absence from the repo.
        assert!(
            body.contains("not absence from the repo"),
            "onboarding instructions must embed the 'not absence' doctrine: {body}"
        );
        assert!(
            body.contains("search_symbols") && body.contains("search_text"),
            "onboarding doctrine must point at search_symbols / search_text: {body}"
        );
    }

    #[test]
    fn test_architecture_map_instructions_embed_orientation_doctrine() {
        let body = build_architecture_map_instructions("prompt_project", None);
        // Statement 1: the map orients; the tools prove.
        assert!(
            body.contains("map orients"),
            "architecture instructions must embed the 'map orients' doctrine: {body}"
        );
        // Statement 2: absence from the map is not absence from the repo.
        assert!(
            body.contains("not absence from the repo"),
            "architecture instructions must embed the 'not absence' doctrine: {body}"
        );
        assert!(
            body.contains("search_symbols") && body.contains("search_text"),
            "architecture doctrine must point at search_symbols / search_text: {body}"
        );
    }

    #[test]
    fn test_tool_driven_instructions_are_surface_aware_and_current() {
        let review = build_code_review_instructions(
            "p",
            &CodeReviewPromptInput {
                path: None,
                focus: None,
            },
        );
        let architecture = build_architecture_map_instructions("p", None);
        let triage = build_failure_triage_instructions(
            "p",
            &FailureTriagePromptInput {
                symptom: "boom".to_string(),
                path: None,
            },
        );
        let onboard = build_onboard_instructions("p", None);
        let refactor = build_refactor_instructions(
            "p",
            &RefactorPromptInput {
                goal: "extract validation".to_string(),
                target: None,
            },
        );
        let debug = build_debug_instructions(
            "p",
            &DebugPromptInput {
                error: "panic".to_string(),
                path: None,
            },
        );

        // Every tool-driven body carries the single upfront surface-mapping note so a
        // compact-surface agent knows the step tool names are full-surface spellings
        // and how to route them through `symforge`/`symforge_edit`.
        for (label, body) in [
            ("review", &review),
            ("architecture", &architecture),
            ("triage", &triage),
            ("onboard", &onboard),
            ("refactor", &refactor),
            ("debug", &debug),
        ] {
            assert!(
                body.contains("Surface note:") && body.contains("`symforge_edit`"),
                "{label} body must embed the surface-mapping note: {body}"
            );
        }

        // Current-reality capability claims land only where they help the plan.
        assert!(
            review.contains("detect_impact") && review.contains("caps at 200"),
            "review must mention detect_impact + the what_changed 200 cap: {review}"
        );
        assert!(
            triage.contains("detect_impact") && triage.contains("caps at 200"),
            "triage must mention detect_impact + the what_changed 200 cap: {triage}"
        );
        assert!(
            refactor.contains("detect_impact"),
            "refactor verify step must mention detect_impact: {refactor}"
        );
        assert!(
            debug.contains("get_symbol(name=")
                && debug.contains("detect_impact")
                && debug.contains("caps at 200"),
            "debug must use name-only get_symbol + detect_impact + the 200 cap: {debug}"
        );
    }
}
