use std::path::PathBuf;

use rmcp::handler::server::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::*;
use rmcp::{transport::stdio, ErrorData, ServerHandler, ServiceExt};
use schemars::JsonSchema;
use serde::Deserialize;

// ── Parameter structs ──

#[derive(Debug, Deserialize, JsonSchema)]
pub struct ScanParams {
    /// Force full re-scan ignoring content hashes
    pub full: Option<bool>,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct QueryParams {
    /// Search term (symbol name or keyword)
    pub pattern: String,
    /// Maximum number of results (default: 20)
    pub limit: Option<i64>,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct InspectParams {
    /// Symbol name or numeric ID
    pub symbol: String,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct ImpactParams {
    /// Symbol name or numeric ID
    pub symbol: String,
    /// Maximum traversal depth (default: 3)
    pub depth: Option<i64>,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct WhyParams {
    /// Source symbol name
    pub from: String,
    /// Target symbol name
    pub to: String,
    /// Maximum search depth (default: 5)
    pub depth: Option<i64>,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct ConfigParams {
    /// Action: get, set, or list
    pub action: String,
    /// Config key (required for get/set)
    pub key: Option<String>,
    /// Config value (required for set)
    pub value: Option<String>,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct DiffParams {
    /// Base branch or commit to compare against. Omit for working tree diff.
    pub base: Option<String>,
    /// Impact analysis depth (default: 3)
    pub depth: Option<i64>,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct ExportParams {
    /// File path pattern to filter (e.g., "src/db")
    pub file: Option<String>,
    /// Approximate token limit for output
    pub token_budget: Option<i64>,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct SinceParams {
    /// Git reference to compare against (SHA, branch, tag, HEAD~N)
    pub reference: String,
    /// Max depth for transitive impact analysis (default: 3)
    pub depth: Option<i64>,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct DigestParams {
    /// Max depth for transitive impact analysis (default: 3)
    pub depth: Option<i64>,
}

// ── MCP Server ──

#[derive(Clone)]
#[allow(dead_code)]
pub struct AstmapMcp {
    project_dir: PathBuf,
    tool_router: ToolRouter<Self>,
}

impl AstmapMcp {
    pub fn new(project_dir: PathBuf) -> Self {
        Self {
            project_dir,
            tool_router: Self::tool_router(),
        }
    }
}

#[rmcp::tool_router]
impl AstmapMcp {
    #[rmcp::tool(
        description = "Build or update the code structure index. Run this once before using other astmap tools, or after major code changes. Incremental by default (only re-parses changed files). Use full=true after branch switches or rebases."
    )]
    async fn scan(
        &self,
        Parameters(params): Parameters<ScanParams>,
    ) -> Result<CallToolResult, ErrorData> {
        let result = astmap::ops::scan(&self.project_dir, params.full.unwrap_or(false))
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        let text = format!(
            "Scan complete: {} files, {} symbols, {} dependencies ({} skipped, {} partial)",
            result.files_total,
            result.symbols,
            result.dependencies,
            result.files_skipped,
            result.files_partial
        );
        Ok(CallToolResult::success(vec![Content::text(text)]))
    }

    #[rmcp::tool(
        description = "Find functions, structs, classes, methods, or types by name. Returns structured results with file path, kind, signature, and line numbers. Use this instead of grep when you need to locate a symbol definition quickly."
    )]
    async fn query(
        &self,
        Parameters(params): Parameters<QueryParams>,
    ) -> Result<CallToolResult, ErrorData> {
        let limit = params.limit.unwrap_or(20);
        let results = astmap::ops::query(&self.project_dir, &params.pattern, limit)
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        let output = serde_json::to_string_pretty(&results)
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        Ok(CallToolResult::success(vec![Content::text(output)]))
    }

    #[rmcp::tool(
        description = "Get a symbol's signature, location (file + line range), and nested children (methods, fields). Use this to understand a struct/class/module structure without reading the entire file."
    )]
    async fn inspect(
        &self,
        Parameters(params): Parameters<InspectParams>,
    ) -> Result<CallToolResult, ErrorData> {
        match astmap::ops::inspect(&self.project_dir, &params.symbol) {
            Ok(result) => {
                let output = serde_json::to_string_pretty(&result)
                    .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
                Ok(CallToolResult::success(vec![Content::text(output)]))
            }
            Err(e) if e.to_string().contains("symbol not found") => {
                Ok(CallToolResult::success(vec![Content::text(format!(
                    "Symbol not found: {}",
                    params.symbol
                ))]))
            }
            Err(e) => Err(ErrorData::internal_error(e.to_string(), None)),
        }
    }

    #[rmcp::tool(
        description = "BEFORE modifying a symbol, run this to see what breaks. Traces transitive dependencies: if you change function A, this returns every function, struct, and module that directly or indirectly depends on A. Essential for safe refactoring."
    )]
    async fn impact(
        &self,
        Parameters(params): Parameters<ImpactParams>,
    ) -> Result<CallToolResult, ErrorData> {
        let depth = params.depth.unwrap_or(3);
        match astmap::ops::impact(&self.project_dir, &params.symbol, depth) {
            Ok(result) => {
                let output = serde_json::to_string_pretty(&result.entries)
                    .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
                Ok(CallToolResult::success(vec![Content::text(output)]))
            }
            Err(e) if e.to_string().contains("symbol not found") => {
                Ok(CallToolResult::success(vec![Content::text(format!(
                    "Symbol not found: {}",
                    params.symbol
                ))]))
            }
            Err(e) => Err(ErrorData::internal_error(e.to_string(), None)),
        }
    }

    #[rmcp::tool(
        description = "Trace why two symbols are connected. Returns the dependency chain (A imports B, B calls C, ...). Use when you need to understand coupling between modules or debug unexpected dependencies."
    )]
    async fn why(
        &self,
        Parameters(params): Parameters<WhyParams>,
    ) -> Result<CallToolResult, ErrorData> {
        let depth = params.depth.unwrap_or(5);
        match astmap::ops::why_path(&self.project_dir, &params.from, &params.to, depth) {
            Ok(result) => {
                if result.path.is_empty() {
                    Ok(CallToolResult::success(vec![Content::text(format!(
                        "No dependency path found between {} and {}",
                        params.from, params.to
                    ))]))
                } else {
                    let output = serde_json::to_string_pretty(&result.path)
                        .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
                    Ok(CallToolResult::success(vec![Content::text(output)]))
                }
            }
            Err(e) if e.to_string().contains("symbol not found") => Ok(CallToolResult::success(
                vec![Content::text("One or both symbols not found".to_string())],
            )),
            Err(e) => Err(ErrorData::internal_error(e.to_string(), None)),
        }
    }

    #[rmcp::tool(description = "Read or update astmap configuration. Actions: get, set, list.")]
    async fn config(
        &self,
        Parameters(params): Parameters<ConfigParams>,
    ) -> Result<CallToolResult, ErrorData> {
        match params.action.as_str() {
            "get" => {
                let key = params
                    .key
                    .ok_or_else(|| ErrorData::invalid_params("key required", None))?;
                let val = astmap::ops::config_get(&self.project_dir, &key)
                    .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
                Ok(CallToolResult::success(vec![Content::text(
                    val.unwrap_or_else(|| format!("Key not found: {}", key)),
                )]))
            }
            "set" => {
                let key = params
                    .key
                    .ok_or_else(|| ErrorData::invalid_params("key required", None))?;
                let value = params
                    .value
                    .ok_or_else(|| ErrorData::invalid_params("value required", None))?;
                astmap::ops::config_set(&self.project_dir, &key, &value)
                    .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
                Ok(CallToolResult::success(vec![Content::text(format!(
                    "Set {} = {}",
                    key, value
                ))]))
            }
            "list" => {
                let configs = astmap::ops::config_list(&self.project_dir)
                    .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
                Ok(CallToolResult::success(vec![Content::text(
                    serde_json::to_string_pretty(&configs).unwrap_or_default(),
                )]))
            }
            _ => Err(ErrorData::invalid_params(
                format!("Unknown action: {}", params.action),
                None,
            )),
        }
    }

    #[rmcp::tool(
        description = "Show which symbols were changed by a git diff and what else they affect transitively. Use without 'base' to check uncommitted changes, or with base='main' to see full branch impact. Ideal before committing or opening a PR."
    )]
    async fn diff(
        &self,
        Parameters(params): Parameters<DiffParams>,
    ) -> Result<CallToolResult, ErrorData> {
        let depth = params.depth.unwrap_or(3);
        let result = astmap::ops::diff(&self.project_dir, params.base.as_deref(), depth)
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        let output = serde_json::to_string_pretty(&result).unwrap_or_default();
        Ok(CallToolResult::success(vec![Content::text(output)]))
    }

    #[rmcp::tool(
        description = "Export the full code structure (files, symbols, signatures, dependencies) as Markdown. Use 'file' to filter by path pattern (e.g. 'src/db'). Use 'token_budget' to cap output size for LLM context windows."
    )]
    async fn export(
        &self,
        Parameters(params): Parameters<ExportParams>,
    ) -> Result<CallToolResult, ErrorData> {
        let md = astmap::ops::export_md(
            &self.project_dir,
            params.file.as_deref(),
            params.token_budget,
        )
        .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        Ok(CallToolResult::success(vec![Content::text(md)]))
    }

    #[rmcp::tool(
        description = "Show which symbols were added, removed, or modified since a git reference, with transitive downstream impact. Use to understand what structurally changed between commits or branches. Example: reference='HEAD~3' or reference='main'."
    )]
    async fn since(
        &self,
        Parameters(params): Parameters<SinceParams>,
    ) -> Result<CallToolResult, ErrorData> {
        let depth = params.depth.unwrap_or(3);
        let result = astmap::ops::since(&self.project_dir, &params.reference, depth)
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        let output = serde_json::to_string_pretty(&result).unwrap_or_default();
        Ok(CallToolResult::success(vec![Content::text(output)]))
    }

    #[rmcp::tool(
        description = "Structural digest since last session. Auto-detects the session boundary and shows what changed in a compact format. Run at the start of a coding session to understand what happened since you were last here. Includes both committed and uncommitted changes."
    )]
    async fn digest(
        &self,
        Parameters(params): Parameters<DigestParams>,
    ) -> Result<CallToolResult, ErrorData> {
        let depth = params.depth.unwrap_or(3);
        let result = astmap::ops::digest(&self.project_dir, depth)
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;

        let mut output = serde_json::to_value(&result.since).unwrap_or_default();
        if let Some(uc) = &result.uncommitted {
            output["uncommitted_changes"] = serde_json::to_value(uc).unwrap_or_default();
        }
        let formatted = serde_json::to_string_pretty(&output).unwrap_or_default();
        Ok(CallToolResult::success(vec![Content::text(formatted)]))
    }
}

#[rmcp::tool_handler]
impl ServerHandler for AstmapMcp {
    fn get_info(&self) -> ServerInfo {
        let capabilities = ServerCapabilities::builder().enable_tools().build();
        ServerInfo::new(capabilities).with_instructions(
            "astmap: Code structure index with transitive impact analysis.\n\n\
             ## Workflow\n\
             1. Run 'scan' at the start of a task to ensure the index is up to date.\n\
             2. Use 'query' instead of grep/glob to find symbol definitions.\n\
             3. Use 'inspect' to see a symbol's callers, callees, and children.\n\
             4. BEFORE modifying a symbol: run 'impact' to check the blast radius.\n\
             5. Use 'why' to trace dependency chains between two symbols.\n\
             6. AFTER making changes: run 'diff' to verify structural changes and transitive impact.\n\
             7. Use 'export' to get a full structural overview.\n\n\
             ## Rules\n\
             - NEVER use grep/glob to search for function/struct definitions or call sites.\n\
             - ALWAYS run 'impact' before renaming, deleting, or changing any public symbol's signature.\n\
             - You MAY use grep/read for text-level searches (error messages, string literals, config values).",
        )
    }
}

pub async fn run_mcp_server(project_dir: PathBuf) -> anyhow::Result<()> {
    let server = AstmapMcp::new(project_dir);
    let service = server.serve(stdio()).await?;
    service.waiting().await?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use rmcp::handler::server::wrapper::Parameters;
    use std::fs;

    fn setup_scanned_project() -> tempfile::TempDir {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(
            src.join("main.rs"),
            r#"use std::io;
fn main() { helper(); }
fn helper() -> i32 { 42 }
struct Config { x: i32 }
"#,
        )
        .unwrap();
        fs::write(
            src.join("lib.rs"),
            r#"pub fn public_fn() -> String { "hello".to_string() }
"#,
        )
        .unwrap();
        astmap::ops::scan(tmp.path(), true).unwrap();
        tmp
    }

    fn setup_git_project() -> tempfile::TempDir {
        let tmp = setup_scanned_project();
        let repo = git2::Repository::init(tmp.path()).unwrap();
        let mut index = repo.index().unwrap();
        index.add_path(std::path::Path::new("src/main.rs")).unwrap();
        index.add_path(std::path::Path::new("src/lib.rs")).unwrap();
        index.write().unwrap();
        let oid = index.write_tree().unwrap();
        let tree = repo.find_tree(oid).unwrap();
        let sig = git2::Signature::now("Test", "test@test.com").unwrap();
        repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
            .unwrap();
        tmp
    }

    #[tokio::test]
    async fn test_mcp_scan() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("main.rs"), "fn main() {}").unwrap();

        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .scan(Parameters(ScanParams { full: Some(true) }))
            .await
            .unwrap();
        let text = &result.content[0];
        assert!(format!("{:?}", text).contains("Scan complete"));
    }

    #[tokio::test]
    async fn test_mcp_scan_incremental() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .scan(Parameters(ScanParams { full: None }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("Scan complete"));
    }

    #[tokio::test]
    async fn test_mcp_query() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .query(Parameters(QueryParams {
                pattern: "helper".to_string(),
                limit: Some(10),
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("helper"));
    }

    #[tokio::test]
    async fn test_mcp_query_default_limit() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .query(Parameters(QueryParams {
                pattern: "main".to_string(),
                limit: None,
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_inspect_by_name() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .inspect(Parameters(InspectParams {
                symbol: "Config".to_string(),
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("Config"));
    }

    #[tokio::test]
    async fn test_mcp_inspect_by_id() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .inspect(Parameters(InspectParams {
                symbol: "1".to_string(),
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_inspect_not_found() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .inspect(Parameters(InspectParams {
                symbol: "NonExistentSymbol".to_string(),
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("not found") || text.contains("Not found"));
    }

    #[tokio::test]
    async fn test_mcp_impact() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .impact(Parameters(ImpactParams {
                symbol: "helper".to_string(),
                depth: Some(3),
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_impact_default_depth() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .impact(Parameters(ImpactParams {
                symbol: "main".to_string(),
                depth: None,
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_impact_not_found() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .impact(Parameters(ImpactParams {
                symbol: "nonexistent".to_string(),
                depth: None,
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("not found") || text.contains("Not found"));
    }

    #[tokio::test]
    async fn test_mcp_why() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .why(Parameters(WhyParams {
                from: "main".to_string(),
                to: "helper".to_string(),
                depth: Some(5),
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_why_default_depth() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .why(Parameters(WhyParams {
                from: "main".to_string(),
                to: "helper".to_string(),
                depth: None,
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_why_not_found() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .why(Parameters(WhyParams {
                from: "nonexistent1".to_string(),
                to: "nonexistent2".to_string(),
                depth: None,
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("not found"));
    }

    #[tokio::test]
    async fn test_mcp_impact_by_id() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let results = astmap::ops::query(tmp.path(), "helper", 1).unwrap();
        assert!(!results.is_empty());
        let id = results[0].id;
        let result = mcp
            .impact(Parameters(ImpactParams {
                symbol: id.to_string(),
                depth: Some(3),
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
        let text = format!("{:?}", &result.content[0]);
        assert!(!text.contains("not found"));
    }

    #[tokio::test]
    async fn test_mcp_impact_invalid_id() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .impact(Parameters(ImpactParams {
                symbol: "999999".to_string(),
                depth: None,
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("not found"));
    }

    #[tokio::test]
    async fn test_mcp_why_by_id() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .why(Parameters(WhyParams {
                from: "999999".to_string(),
                to: "999998".to_string(),
                depth: None,
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("not found"));
    }

    #[tokio::test]
    async fn test_mcp_config_set_get_list() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());

        // Set
        let result = mcp
            .config(Parameters(ConfigParams {
                action: "set".to_string(),
                key: Some("test.key".to_string()),
                value: Some("test_value".to_string()),
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("test.key"));

        // Get
        let result = mcp
            .config(Parameters(ConfigParams {
                action: "get".to_string(),
                key: Some("test.key".to_string()),
                value: None,
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("test_value"));

        // List
        let result = mcp
            .config(Parameters(ConfigParams {
                action: "list".to_string(),
                key: None,
                value: None,
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_config_get_not_found() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .config(Parameters(ConfigParams {
                action: "get".to_string(),
                key: Some("nonexistent".to_string()),
                value: None,
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("not found") || text.contains("Not found"));
    }

    #[tokio::test]
    async fn test_mcp_config_missing_key() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .config(Parameters(ConfigParams {
                action: "get".to_string(),
                key: None,
                value: None,
            }))
            .await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_mcp_config_set_missing_value() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .config(Parameters(ConfigParams {
                action: "set".to_string(),
                key: Some("k".to_string()),
                value: None,
            }))
            .await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_mcp_config_unknown_action() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .config(Parameters(ConfigParams {
                action: "delete".to_string(),
                key: None,
                value: None,
            }))
            .await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_mcp_diff_working_tree() {
        let tmp = setup_git_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .diff(Parameters(DiffParams {
                base: None,
                depth: Some(3),
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_diff_default_depth() {
        let tmp = setup_git_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .diff(Parameters(DiffParams {
                base: None,
                depth: None,
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_diff_with_base() {
        let tmp = setup_git_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());

        let repo = git2::Repository::open(tmp.path()).unwrap();
        let head = repo.head().unwrap().peel_to_commit().unwrap();
        let base_oid = head.id().to_string();

        fs::write(
            tmp.path().join("src/main.rs"),
            "fn main() { println!(\"changed\"); }\n",
        )
        .unwrap();
        let mut index = repo.index().unwrap();
        index.add_path(std::path::Path::new("src/main.rs")).unwrap();
        index.write().unwrap();
        let oid = index.write_tree().unwrap();
        let tree = repo.find_tree(oid).unwrap();
        let sig = git2::Signature::now("Test", "test@test.com").unwrap();
        repo.commit(Some("HEAD"), &sig, &sig, "change", &tree, &[&head])
            .unwrap();

        astmap::ops::scan(tmp.path(), true).unwrap();

        let result = mcp
            .diff(Parameters(DiffParams {
                base: Some(base_oid),
                depth: Some(3),
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_export() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .export(Parameters(ExportParams {
                file: None,
                token_budget: None,
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("Code Structure Index"));
    }

    #[tokio::test]
    async fn test_mcp_export_with_filter() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .export(Parameters(ExportParams {
                file: Some("main".to_string()),
                token_budget: None,
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("main"));
    }

    #[tokio::test]
    async fn test_mcp_export_with_budget() {
        let tmp = setup_scanned_project();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .export(Parameters(ExportParams {
                file: None,
                token_budget: Some(5000),
            }))
            .await
            .unwrap();
        assert!(!result.content.is_empty());
    }

    #[tokio::test]
    async fn test_mcp_why_no_path() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("a.rs"), "fn alpha() -> i32 { 1 }").unwrap();
        fs::write(src.join("b.rs"), "fn beta() -> i32 { 2 }").unwrap();
        astmap::ops::scan(tmp.path(), true).unwrap();

        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .why(Parameters(WhyParams {
                from: "alpha".to_string(),
                to: "beta".to_string(),
                depth: None,
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("No dependency path"));
    }

    #[tokio::test]
    async fn test_mcp_inspect_with_children() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(
            src.join("main.rs"),
            r#"pub struct Foo { x: i32 }
impl Foo {
    pub fn new(x: i32) -> Self { Foo { x } }
    pub fn get_x(&self) -> i32 { self.x }
}
"#,
        )
        .unwrap();
        astmap::ops::scan(tmp.path(), true).unwrap();

        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .inspect(Parameters(InspectParams {
                symbol: "Foo".to_string(),
            }))
            .await
            .unwrap();
        let text = format!("{:?}", &result.content[0]);
        assert!(text.contains("Foo"));
    }

    #[test]
    fn test_mcp_get_info() {
        let tmp = tempfile::tempdir().unwrap();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let info = mcp.get_info();
        assert!(format!("{:?}", info).contains("astmap"));
    }

    #[tokio::test]
    async fn test_mcp_open_db_no_existing() {
        let tmp = tempfile::tempdir().unwrap();
        let mcp = AstmapMcp::new(tmp.path().to_path_buf());
        let result = mcp
            .query(Parameters(QueryParams {
                pattern: "test".to_string(),
                limit: None,
            }))
            .await;
        assert!(result.is_err());
    }
}
