use serde_json::{json, Value};

use crate::db;

// ── 工具 Schema 定义 ──────────────────────────────────────────────────────────

pub fn tools_list_result() -> Value {
    json!({
        "tools": [
            {
                "name": "get_diagnostics",
                "description": "查询炼境运行时诊断信息：MCP Server 启动状态、各项目 MCP 配置注入结果、最近错误日志。排查功能是否正常时首先调用此工具。",
                "inputSchema": { "type": "object", "properties": {}, "required": [] }
            },
            {
                "name": "list_group_projects",
                "description": "列出产品组内所有成员项目，包含名称、路径、平台、技术栈、角色等信息（group_id 可从 list_all_projects 获取）",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "group_id": { "type": "string", "description": "产品组 ID" }
                    },
                    "required": ["group_id"]
                }
            },
            {
                "name": "get_project_blueprint",
                "description": "读取产品组内某个项目的蓝图文件（.blueprint/manifest.yaml），了解该项目的模块与任务状态",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "get_project_file",
                "description": "读取产品组内某个项目的任意文件内容，如 openapi.yaml、README.md、src/types.ts 等",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID" },
                        "relative_path": { "type": "string", "description": "相对于项目根目录的文件路径，如 openapi.yaml 或 src/api/routes.ts" }
                    },
                    "required": ["project_id", "relative_path"]
                }
            },
            {
                "name": "get_project_mentor_context",
                "description": "获取产品组内某个项目的完整导师上下文——包含健康状态、蓝图进度摘要、近期错误日志，以及格式化好的 Markdown 报告（可直接用于质量分析和成长建议）",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "append_project_note",
                "description": "将 Claude 的分析结论或复盘记录写入项目笔记，供未来会话和用户在炼境中查阅。完成 M/L 复杂度任务后必须调用，写入结构化复盘笔记。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "content": {
                            "type": "string",
                            "description": "笔记内容。完成 M/L 任务后使用以下结构化格式：\n【复盘】<任务标题>\n任务类型: 后端命令|前端组件|数据层|MCP工具|架构调整\n复杂度: S|M|L\n实际轮数: N轮（预估M轮）\n是否返工: 是/否\n返工原因: <若返工填写，否则省略>\n有效策略: <本次奏效的做法>\n遗留风险: <若有填写，否则省略>\n\n其他场景（分析结论、建议、洞察）可自由格式。"
                        }
                    },
                    "required": ["project_id", "content"]
                }
            },
            {
                "name": "get_blueprint_status",
                "description": "检测项目蓝图治理状态：CONVENTIONS 规则版本是否最新、蓝图内容是否可能落后于代码（synced/rules_outdated/content_stale/none）。等价于炼境 UI 项目卡片上的蓝图状态徽章。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "generate_blueprint_prompt",
                "description": "生成蓝图初始化/同步提示词（含 CONVENTIONS 规范与 manifest 严格格式约束）。等价于炼境 UI 治理面板的「初始化蓝图/更新蓝图描绘」按钮。拿到提示词后在该项目目录下执行它（或交给该项目的 AI 会话），完成后可用 get_blueprint_status / get_project_blueprint 验证结果。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "mode": { "type": "string", "enum": ["init", "sync"], "description": "init=项目尚无蓝图时从零初始化；sync=已有蓝图，对照代码现状同步更新" }
                    },
                    "required": ["project_id", "mode"]
                }
            },
            {
                "name": "sync_blueprint_rules",
                "description": "将最新 CONVENTIONS 规则同步到项目（写入 .blueprint/CONVENTIONS.md + 更新 AGENTS.md/CLAUDE.md 的蓝图管理区块）。等价于炼境 UI 治理面板的「同步规则」按钮。仅当 get_blueprint_status 返回 rules_outdated 时需要调用。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "create_pull_request",
                "description": "通过炼境存储的 Gitea 凭证为项目创建合并请求（PR）。炼境内部调用 Gitea API，无需 Claude 自行提取 token。/ship 交付流程在推送分支后应调用此工具完成 PR 创建。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "head": { "type": "string", "description": "源分支名，如 dev" },
                        "base": { "type": "string", "description": "目标分支名，如 main" },
                        "title": { "type": "string", "description": "PR 标题" },
                        "body": { "type": "string", "description": "PR 描述（Markdown），可为空字符串" }
                    },
                    "required": ["project_id", "head", "base", "title"]
                }
            },
            {
                "name": "get_project_snapshots",
                "description": "获取项目的历史健康快照（每日一条），包含 git 提交间隔、蓝图完成率、阻塞任务数、错误情况。用于分析趋势、识别高价值改进变量。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "days": { "type": "integer", "description": "查询最近 N 天的快照，默认 30" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "poll_now",
                "description": "立即触发一次 Gitea 轮询：拉取所有绑定仓库的最新 commit 和 PR 状态写入飞轮。等价于炼境 UI「立即拉取」按钮。适用于公网 Gitea + 内网炼境（webhook 不可达）场景。",
                "inputSchema": { "type": "object", "properties": {}, "required": [] }
            },
            {
                "name": "setup_webhook",
                "description": "为项目重新注册 Gitea webhook（订阅 push + pull_request 事件）。等价于炼境 UI「重设」按钮。导入新项目后、或需要更新事件订阅时调用。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "inject_mcp",
                "description": "向项目的 .claude/settings.json 注入炼境 MCP 配置，同时写入 bypassPermissions 和 liangjing.json。等价于炼境 UI「注入 MCP」按钮。导入新项目后或炼境重启后调用。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "get_ci_status",
                "description": "查询指定 commit 的 CI 检查结果（pass/fail/pending）。/ship 流程推送分支并开 PR 后，用此工具确认 CI 是否通过再决定是否合并。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "commit_sha": { "type": "string", "description": "要查询的 commit SHA（完整 40 位或前 8 位均可）" }
                    },
                    "required": ["project_id", "commit_sha"]
                }
            },
            {
                "name": "merge_pull_request",
                "description": "合并项目的指定 PR。建议在 get_ci_status 确认通过后再调用。等价于 Gitea UI「合并 PR」按钮。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "pr_number": { "type": "integer", "description": "PR 编号（来自 create_pull_request 的返回结果或 Gitea UI）" }
                    },
                    "required": ["project_id", "pr_number"]
                }
            },
            {
                "name": "get_pr_events",
                "description": "查询项目的 PR 生命周期历史（opened/merged/closed）。数据来自炼境飞轮，本地 git 命令无法获取。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "limit": { "type": "integer", "description": "返回最近 N 条记录，默认 20" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "get_commit_metrics",
                "description": "查询项目的提交飞轮数据：提交类型分布（feat/fix/chore...）、是否返工、近期频率。数据来自炼境 DB，本地 git log 无法直接得到解析后的分类。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "days": { "type": "integer", "description": "查询最近 N 天，默认 30" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "distribute_cicd",
                "description": "向项目写入 CI 模板文件（.gitea/workflows/）。当项目缺少 CI 配置时调用，自动解析项目路径写入。支持 pr_ci（PR 质量检查）和 deepseek_review（AI 代码审查）两种类型。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "workflow_type": { "type": "string", "enum": ["pr_ci", "deepseek_review"], "description": "pr_ci=PR 合并前质量检查；deepseek_review=AI 代码审查并评论到 PR" }
                    },
                    "required": ["project_id", "workflow_type"]
                }
            },
            {
                "name": "list_all_projects",
                "description": "列出炼境中所有已登记项目（不限产品组），包含 project_id、名称、路径、所属产品组、MCP 注入状态。用于发现尚未注入的项目，再调 inject_mcp 完成接入。",
                "inputSchema": { "type": "object", "properties": {}, "required": [] }
            },
            {
                "name": "apply_onboarding_pack",
                "description": "向项目分发标准接入包：写入 AGENTS.md / lefthook / git-workflow 模板（已存在则原位升级 ONBOARDING_MANAGED 块，绝不覆盖块外用户内容），并挂载 buff 自动监听 git 提交。等价于炼境 UI「接入包」区域的「应用接入包」按钮。模板版本升级后对存量项目重新调用即可推送新版本。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "verify_onboarding",
                "description": "接入生效验收：检查接入包在项目里是否真正生效（模板管理块版本到位 / lefthook hook 真挂在 .git/hooks / origin 指向已登记 Gitea / CI workflow 存在 / 近 30 天飞轮 commit 数据流入）。分发成功 ≠ 功能生效，apply_onboarding_pack 之后应调用本工具做消费者角色验收。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "scan_unregistered_projects",
                "description": "扫描已登记项目的父目录，找出存在于文件系统但尚未在炼境注册的项目目录。返回每个未注册目录的路径、是否有 .git、技术栈特征文件（package.json / Cargo.toml / go.mod）、是否有 AGENTS.md。用于日常发现漏网项目并决定是否接入炼境。",
                "inputSchema": { "type": "object", "properties": {}, "required": [] }
            },
            {
                "name": "unregister_project",
                "description": "从炼境删除一条项目登记（等价 UI 删除项目按钮的 agent 投影）：清理产品组 MCP 配置、关联表、workspace 与孤儿 profile。绝不删除本地文件（该能力不对 agent 开放）。适用：scan_unregistered_projects 的反向操作——清理目录已消失或已无 .git 的死登记。删除不可逆，调用前先向用户列出目标清单确认。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "要删除登记的项目 workspace ID" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "ingest_git_history",
                "description": "全量导入 git 历史到飞轮（等价 UI「全量导入」按钮的 agent 投影）：解析最近 500 条 commit 的 conventional 指标、Rules-Applied trailer 与触碰文件，UPSERT 幂等（存量行自动回填 trailer）。不传 project_id 时对所有已登记 Windows 路径项目执行。适用：trailer 存量回填、新项目接入后初始化飞轮数据。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "可选。项目 workspace ID；省略则遍历所有已登记项目" }
                    },
                    "required": []
                }
            },
            {
                "name": "predict_task_risk",
                "description": "任务风险预测（飞轮阶段三）：按 scope 和/或文件清单查询该项目历史返工率，返回 risk_level（low/medium/high）、样本量与置信度、热点文件排行。agent 开工前调用——任务卡涉及高返工 scope/文件时提高警惕（多写测试、小步提交）。样本 <5 或 commit 历史不规范时 confidence=low，结论仅供参考。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "scope": { "type": "string", "description": "conventional commit scope（如 onboarding、auth/login），前缀匹配子 scope" },
                        "files": { "type": "array", "items": { "type": "string" }, "description": "任务卡涉及的文件相对路径清单（与 git log --name-only 路径格式一致）" }
                    },
                    "required": ["project_id"]
                }
            },
            {
                "name": "setup_gitea_repo",
                "description": "为项目在默认 Gitea 实例上创建远程仓库，并将本地 git remote origin 指向新仓库（幂等：仓库已存在则直接绑定，remote 已有则 set-url 更新）。创建后炼境自动记录绑定关系，后续可调用 setup_webhook 注册 webhook。需先在炼境 UI 配置一个默认 Gitea 实例。",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "project_id": { "type": "string", "description": "项目 workspace ID（来自 list_group_projects 的返回结果）" },
                        "repo_name": { "type": "string", "description": "Gitea 仓库名；省略则用项目名自动 slug 化（非 ASCII 字符过滤掉）" },
                        "private": { "type": "boolean", "description": "是否创建私有仓库，默认 false（公开）" }
                    },
                    "required": ["project_id"]
                }
            }
        ]
    })
}

// ── 工具分发 ──────────────────────────────────────────────────────────────────

pub async fn tools_call(params: Option<&Value>) -> Value {
    let tool_name = params
        .and_then(|p| p.get("name"))
        .and_then(|n| n.as_str())
        .unwrap_or("")
        .to_string();
    let args = params
        .and_then(|p| p.get("arguments"))
        .cloned()
        .unwrap_or(json!({}));

    let outcome = tokio::task::spawn_blocking(move || match tool_name.as_str() {
        "get_diagnostics" => get_diagnostics(),
        "list_group_projects" => {
            let group_id = args
                .get("group_id")
                .and_then(|v| v.as_str())
                .filter(|s| !s.is_empty());
            match group_id {
                Some(g) => list_group_projects(g),
                None => Err("需显式传 group_id 参数（组 ID 可从 list_all_projects 获取）".to_string()),
            }
        }
        "get_project_blueprint" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            get_project_blueprint(pid)
        }
        "get_blueprint_status" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            resolve_project_root(pid).and_then(|root| {
                crate::commands::blueprint::get_blueprint_status(root.to_string_lossy().to_string())
                    .and_then(|s| serde_json::to_string_pretty(&s).map_err(|e| e.to_string()))
            })
        }
        "generate_blueprint_prompt" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let mode = args.get("mode").and_then(|v| v.as_str()).unwrap_or("sync");
            if mode != "init" && mode != "sync" {
                Err(format!("mode 只允许 init 或 sync，收到: {mode}"))
            } else {
                resolve_project_root(pid).and_then(|root| {
                    crate::commands::blueprint::generate_blueprint_prompt(
                        root.to_string_lossy().to_string(),
                        mode.to_string(),
                    )
                })
            }
        }
        "sync_blueprint_rules" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            resolve_project_root(pid).and_then(|root| {
                crate::commands::blueprint::sync_blueprint_rules(root.to_string_lossy().to_string())
                    .and_then(|r| serde_json::to_string_pretty(&r).map_err(|e| e.to_string()))
            })
        }
        "get_project_file" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let rel = args.get("relative_path").and_then(|v| v.as_str()).unwrap_or("");
            get_project_file(pid, rel)
        }
        "get_project_mentor_context" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            get_project_mentor_context(pid)
        }
        "append_project_note" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");
            append_project_note(pid, content)
        }
        "get_project_snapshots" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let days = args.get("days").and_then(|v| v.as_u64()).unwrap_or(30) as u32;
            get_project_snapshots(pid, days)
        }
        "create_pull_request" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let head = args.get("head").and_then(|v| v.as_str()).unwrap_or("");
            let base = args.get("base").and_then(|v| v.as_str()).unwrap_or("main");
            let title = args.get("title").and_then(|v| v.as_str()).unwrap_or("");
            let body = args.get("body").and_then(|v| v.as_str()).unwrap_or("");
            crate::commands::gitea::gitea_api_create_pull_request(pid, head, base, title, body)
        }
        "poll_now" => crate::commands::gitea::poll_all_repos()
            .map(|(polled, ingested)| format!("轮询完成：{polled} 个仓库，新 commit {ingested} 条，PR 事件已同步")),
        "setup_webhook" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let port = crate::mcp_server::read_port();
            let callback_base = format!("http://127.0.0.1:{}", port);
            crate::commands::gitea::gitea_setup_webhook(pid.to_string(), callback_base).map(|repo| {
                format!(
                    "webhook 注册成功：{}/{} hook_id={}",
                    repo.gitea_owner,
                    repo.gitea_repo_name,
                    repo.webhook_id.map(|id| id.to_string()).unwrap_or_default()
                )
            })
        }
        "inject_mcp" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            crate::mcp_inject::inject_project_mcp(pid)
                .map(|_| format!("MCP 注入成功：project_id={}（单一 lian-jing 服务器，无需 group_id）", pid))
        }
        "get_ci_status" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let sha = args.get("commit_sha").and_then(|v| v.as_str()).unwrap_or("");
            if sha.is_empty() {
                return Err("commit_sha 不能为空".to_string());
            }
            crate::commands::gitea::gitea_api_get_ci_status(pid, sha)
        }
        "merge_pull_request" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let pr_num = args.get("pr_number").and_then(|v| v.as_i64()).unwrap_or(0);
            if pr_num <= 0 {
                return Err("pr_number 必须为正整数".to_string());
            }
            crate::commands::gitea::gitea_api_merge_pull_request(pid, pr_num)
        }
        "get_pr_events" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
            let conn = db::pool().get().map_err(|e| e.to_string())?;
            let mut stmt = conn
                .prepare(
                    "SELECT pr_number, title, action, head_branch, base_branch, author, event_at
                     FROM pr_events WHERE project_id = ?1 ORDER BY event_at DESC LIMIT ?2",
                )
                .map_err(|e| e.to_string())?;
            let rows: Vec<String> = stmt
                .query_map(rusqlite::params![pid, limit as i64], |r| {
                    Ok((
                        r.get::<_, i64>(0)?,
                        r.get::<_, String>(1)?,
                        r.get::<_, String>(2)?,
                        r.get::<_, String>(3)?,
                        r.get::<_, String>(4)?,
                        r.get::<_, String>(5)?,
                        r.get::<_, String>(6)?,
                    ))
                })
                .map_err(|e| e.to_string())?
                .filter_map(|r| r.ok())
                .map(|(num, title, action, head, base, author, at)| {
                    let icon = match action.as_str() {
                        "merged" => "✅",
                        "closed" => "🚫",
                        _ => "🔵",
                    };
                    format!("{icon} PR #{num} [{action}] {title}\n   {head} → {base} by {author} @ {at}")
                })
                .collect();
            if rows.is_empty() {
                Ok(format!("项目 {} 暂无 PR 事件记录", pid))
            } else {
                Ok(format!("PR 事件（最近 {} 条）：\n\n{}", rows.len(), rows.join("\n\n")))
            }
        }
        "get_commit_metrics" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let days = args.get("days").and_then(|v| v.as_u64()).unwrap_or(30);
            let conn = db::pool().get().map_err(|e| e.to_string())?;
            let offset = format!("-{} days", days);
            let total: i64 = conn
                .query_row(
                    "SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1 AND committed_at >= datetime('now', ?2)",
                    rusqlite::params![pid, offset],
                    |r| r.get(0),
                )
                .unwrap_or(0);
            let mut type_stmt = conn
                .prepare(
                    "SELECT COALESCE(commit_type,'unknown') as t, COUNT(*) as n, SUM(is_rework) as rework
                     FROM commit_metrics WHERE project_id = ?1 AND committed_at >= datetime('now', ?2)
                     GROUP BY t ORDER BY n DESC",
                )
                .map_err(|e| e.to_string())?;
            let type_rows: Vec<String> = type_stmt
                .query_map(rusqlite::params![pid, offset], |r| {
                    Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?, r.get::<_, i64>(2)?))
                })
                .map_err(|e| e.to_string())?
                .filter_map(|r| r.ok())
                .map(|(t, n, rework)| format!("  - {t}: {n} 条（返工 {rework} 条）"))
                .collect();
            let rework_total: i64 = conn
                .query_row(
                    "SELECT SUM(is_rework) FROM commit_metrics WHERE project_id = ?1 AND committed_at >= datetime('now', ?2)",
                    rusqlite::params![pid, offset],
                    |r| r.get(0),
                )
                .unwrap_or(0);
            let mut out = format!("## 提交飞轮数据（最近 {} 天）\n\n", days);
            out.push_str(&format!("- **总提交数**：{}\n", total));
            out.push_str(&format!(
                "- **返工提交**：{} 条（{:.0}%）\n",
                rework_total,
                if total > 0 { rework_total as f64 / total as f64 * 100.0 } else { 0.0 }
            ));
            out.push_str("\n**按类型分布**：\n");
            out.push_str(&type_rows.join("\n"));
            Ok(out)
        }
        "distribute_cicd" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let wt = args.get("workflow_type").and_then(|v| v.as_str()).unwrap_or("");
            crate::commands::cicd::distribute_workflow_for_project(pid, wt)
        }
        "list_all_projects" => list_all_projects(),
        "scan_unregistered_projects" => scan_unregistered_projects(),
        "unregister_project" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            if pid.is_empty() {
                Err("project_id 不能为空".to_string())
            } else {
                // delete_local 硬编码 false：MCP 侧永不删除本地文件
                crate::commands::projects::delete_project(pid.to_string(), Some(false))
                    .map(|_| format!("已删除登记：{}（本地文件未动）", pid))
            }
        }
        "ingest_git_history" => {
            let pid = args.get("project_id").and_then(|v| v.as_str());
            let conn = db::pool().get().map_err(|e| e.to_string())?;
            let targets: Vec<(String, String)> = {
                let mut stmt = conn
                    .prepare(
                        "SELECT id, win_path FROM project_workspaces
                         WHERE win_path IS NOT NULL AND (?1 IS NULL OR id = ?1)",
                    )
                    .map_err(|e| e.to_string())?;
                let rows = stmt
                    .query_map(rusqlite::params![pid], |r| Ok((r.get(0)?, r.get(1)?)))
                    .map_err(|e| e.to_string())?
                    .filter_map(|r| r.ok())
                    .collect();
                rows
            };
            drop(conn);
            if targets.is_empty() {
                Err(format!("未找到可导入的项目（project_id={:?}）", pid))
            } else {
                let mut lines = Vec::new();
                let (mut ok, mut failed) = (0u32, 0u32);
                for (id, path) in &targets {
                    match crate::commands::commit_metrics::ingest_full_git_history(
                        id.clone(),
                        path.clone(),
                    ) {
                        Ok(r) => {
                            ok += 1;
                            lines.push(format!("- {}: 写入 {} 条，跳过 {} 条", id, r.imported, r.skipped));
                        }
                        Err(e) => {
                            failed += 1;
                            lines.push(format!("- {}: ❌ {}", id, e));
                        }
                    }
                }
                Ok(format!(
                    "全量导入完成：{} 个项目成功，{} 个失败\n\n{}",
                    ok,
                    failed,
                    lines.join("\n")
                ))
            }
        }
        "predict_task_risk" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let scope = args.get("scope").and_then(|v| v.as_str());
            let files: Vec<String> = args
                .get("files")
                .and_then(|v| v.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|f| f.as_str().map(str::to_string))
                        .collect()
                })
                .unwrap_or_default();
            let conn = db::pool().get().map_err(|e| e.to_string())?;
            crate::commands::risk::predict_task_risk(&conn, pid, scope, &files)
                .and_then(|r| serde_json::to_string_pretty(&r).map_err(|e| e.to_string()))
        }
        "setup_gitea_repo" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let repo_name = args.get("repo_name").and_then(|v| v.as_str());
            let private = args.get("private").and_then(|v| v.as_bool()).unwrap_or(false);
            crate::commands::gitea::gitea_api_setup_repo(pid, repo_name, private)
        }
        "apply_onboarding_pack" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            resolve_project_root(pid).and_then(|root| {
                let path = root.to_string_lossy().to_string();
                crate::commands::onboarding::apply_onboarding_pack(path, pid.to_string()).map(
                    |report| {
                        let template_lines: Vec<String> = report
                            .templates
                            .iter()
                            .map(|t| {
                                let icon = match t.action.as_str() {
                                    "written" => "✅ written",
                                    "updated" => "🔄 updated",
                                    "appended" => "➕ appended",
                                    "skipped" => "⏭ skipped",
                                    _ => "❌ failed",
                                };
                                format!("  {icon}: {}", t.name)
                            })
                            .collect();
                        let hook_icon = match report.hook.status.as_str() {
                            "activated" => "✅",
                            "missing_binary" => "⚠️",
                            _ => "❌",
                        };
                        format!(
                            "接入包分发完成（project_id={}）\n\n模板：\n{}\n\nlefthook hook：{} {}（{}）\nbuff：{}\n\n建议调用 verify_onboarding 做生效验收。",
                            pid,
                            template_lines.join("\n"),
                            hook_icon,
                            report.hook.status,
                            report.hook.detail,
                            report.buff.status
                        )
                    },
                )
            })
        }
        "verify_onboarding" => {
            let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
            let root = resolve_project_root(pid)?;
            let conn = db::pool().get().map_err(|e| e.to_string())?;
            let checks = crate::commands::onboarding::verify_onboarding(&conn, pid, &root);
            let passed = checks.iter().filter(|c| c.pass).count();
            serde_json::to_string_pretty(&json!({
                "project_id": pid,
                "pass": passed == checks.len(),
                "summary": format!("{passed}/{} 项通过", checks.len()),
                "checks": checks,
            }))
            .map_err(|e| e.to_string())
        }
        _ => Err(format!("未知工具: {}", tool_name)),
    })
    .await;

    match outcome {
        Ok(Ok(text)) => json!({ "content": [{ "type": "text", "text": text }] }),
        Ok(Err(e)) => json!({ "content": [{ "type": "text", "text": e }], "isError": true }),
        Err(e) => json!({ "content": [{ "type": "text", "text": e.to_string() }], "isError": true }),
    }
}

// ── 工具实现 ──────────────────────────────────────────────────────────────────

fn get_diagnostics() -> Result<String, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    let port = crate::mcp_server::read_port();

    let start_time: Option<String> = conn
        .query_row(
            "SELECT ts FROM runtime_logs WHERE category = 'mcp_server' AND level = 'info' ORDER BY id ASC LIMIT 1",
            [],
            |r| r.get(0),
        )
        .ok();

    let mut out = String::new();
    out.push_str("## 炼境 MCP 诊断报告\n\n");
    out.push_str(&format!("- **服务端口**: {}\n", port));
    out.push_str(&format!(
        "- **启动时间**: {}\n\n",
        start_time.as_deref().unwrap_or("未知（可能在本次日志之前启动）")
    ));

    out.push_str("### MCP 注入状态（每个项目最后一次结果）\n\n");
    let mut stmt = conn
        .prepare(
            "SELECT context, level, message, ts
             FROM runtime_logs
             WHERE category = 'mcp_inject'
               AND id IN (
                   SELECT MAX(id) FROM runtime_logs
                   WHERE category = 'mcp_inject'
                   GROUP BY json_extract(context, '$.project_id')
               )
             ORDER BY ts DESC
             LIMIT 50",
        )
        .map_err(|e| e.to_string())?;

    let inject_rows: Vec<(Option<String>, String, String, String)> = stmt
        .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)))
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    if inject_rows.is_empty() {
        out.push_str("_暂无注入记录（成员加入产品组时会产生）_\n\n");
    } else {
        for (ctx, level, msg, ts) in &inject_rows {
            let project_id = ctx
                .as_deref()
                .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
                .and_then(|v| v.get("project_id").and_then(|x| x.as_str()).map(|s| s.to_string()))
                .unwrap_or_else(|| "unknown".to_string());
            let icon = if level == "info" { "✅" } else { "⚠️" };
            out.push_str(&format!("- {} `{}` — {} _({})\n", icon, project_id, msg, ts));
        }
        out.push('\n');
    }

    out.push_str("### 最近错误日志（最多 20 条）\n\n");
    let mut stmt2 = conn
        .prepare(
            "SELECT ts, category, level, message FROM runtime_logs
             WHERE level IN ('error', 'warn')
             ORDER BY id DESC LIMIT 20",
        )
        .map_err(|e| e.to_string())?;

    let error_rows: Vec<(String, String, String, String)> = stmt2
        .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)))
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    if error_rows.is_empty() {
        out.push_str("_无错误记录_ ✅\n");
    } else {
        for (ts, cat, level, msg) in &error_rows {
            out.push_str(&format!("- `[{}]` **{}** `{}` — {}\n", ts, level, cat, msg));
        }
    }

    out.push_str("\n### 系统健康\n\n");
    let report = crate::commands::settings::collect_health_report();
    out.push_str(&format!("检查时间：{}\n\n", report.checked_at));
    out.push_str("| 检查项 | 状态 | 详情 |\n|--------|------|------|\n");
    for item in &report.items {
        let icon = match item.status.as_str() {
            "ok" => "✅",
            "warn" => "⚠️",
            "error" => "❌",
            _ => "—",
        };
        out.push_str(&format!(
            "| {} | {} {} | {} |\n",
            item.name,
            icon,
            item.status,
            item.detail.as_deref().unwrap_or("-")
        ));
    }

    out.push_str("\n### 质量指标\n\n");
    let q = &report.quality;
    out.push_str(&format!("- **休眠项目**（>30天未 push）：{} 个\n", q.dormant_projects.len()));
    for p in &q.dormant_projects {
        out.push_str(&format!("  - {} （{} 天）\n", p.name, p.days_since_push));
    }
    out.push_str(&format!("- **孤立项目**（无分组 & 无 repo）：{} 个\n", q.orphaned_projects.len()));
    for p in &q.orphaned_projects {
        out.push_str(&format!("  - {}\n", p.name));
    }
    out.push_str(&format!(
        "- **MCP 注入异常产品组**（近 7 天）：{} 个\n",
        q.groups_with_inject_warn.len()
    ));
    for gid in &q.groups_with_inject_warn {
        out.push_str(&format!("  - `{}`\n", gid));
    }
    out.push_str(&format!("- **无本地路径项目**：{} 个\n", q.projects_without_path));

    Ok(out)
}

fn list_group_projects(group_id: &str) -> Result<String, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    let mut stmt = conn
        .prepare(
            "SELECT pw.id, pp.name, pw.platform, pw.win_path, pw.wsl_path, pp.tech_stack, pgm.role
             FROM project_group_map pgm
             JOIN project_workspaces pw ON pw.id = pgm.project_id
             JOIN project_profiles   pp ON pp.id = pw.profile_id
             WHERE pgm.group_id = ?1",
        )
        .map_err(|e| e.to_string())?;

    let rows: Vec<String> = stmt
        .query_map([group_id], |row| {
            let id: String = row.get(0)?;
            let name: String = row.get(1)?;
            let platform: Option<String> = row.get(2)?;
            let win_path: Option<String> = row.get(3)?;
            let wsl_path: Option<String> = row.get(4)?;
            let tech_stack: Option<String> = row.get(5)?;
            let role: Option<String> = row.get(6)?;
            Ok(format!(
                "- **{}** (id: `{}`)\n  平台: {} | 技术栈: {}{}\n  Windows 路径: {}\n  WSL 路径: {}",
                name,
                id,
                platform.as_deref().unwrap_or("unknown"),
                tech_stack.as_deref().unwrap_or("未配置"),
                role.map(|r| format!(" | 角色: {}", r)).unwrap_or_default(),
                win_path.as_deref().unwrap_or("未配置"),
                wsl_path.as_deref().unwrap_or("未配置"),
            ))
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    if rows.is_empty() {
        return Ok("该产品组暂无成员项目。".to_string());
    }
    Ok(format!("产品组成员（共 {} 个项目）：\n\n{}", rows.len(), rows.join("\n\n")))
}

fn get_project_blueprint(project_id: &str) -> Result<String, String> {
    let root = resolve_project_root(project_id)?;
    let path = root.join(".blueprint").join("manifest.yaml");
    std::fs::read_to_string(&path)
        .map_err(|e| format!("读取蓝图失败（{}）: {}", path.display(), e))
}

fn get_project_file(project_id: &str, relative_path: &str) -> Result<String, String> {
    if relative_path.contains("..") {
        return Err("路径不允许包含 \"..\"".to_string());
    }
    let root = resolve_project_root(project_id)?;
    let path = root.join(relative_path);
    std::fs::read_to_string(&path)
        .map_err(|e| format!("读取文件失败（{}）: {}", path.display(), e))
}

/// 按 project_id 直查项目根目录（单一服务器寻址，不做组归属过滤）
pub fn resolve_project_root(project_id: &str) -> Result<std::path::PathBuf, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    let row: rusqlite::Result<(Option<String>, Option<String>, Option<String>)> = conn.query_row(
        "SELECT win_path, wsl_path, platform FROM project_workspaces WHERE id = ?1",
        [project_id],
        |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
    );
    let (win_path, wsl_path, platform) = row
        .map_err(|_| format!("项目 {} 不存在", project_id))?;

    if platform.as_deref() == Some("wsl") {
        let wsl = wsl_path.ok_or_else(|| "WSL 项目未配置 wsl_path".to_string())?;
        let distro: String = conn
            .query_row("SELECT value FROM settings WHERE key = 'wsl_distro'", [], |r| r.get(0))
            .unwrap_or_else(|_| "Ubuntu".to_string());
        let inner = wsl.trim_start_matches('/').replace('/', "\\");
        Ok(std::path::PathBuf::from(format!("\\\\wsl.localhost\\{}\\{}", distro, inner)))
    } else {
        let win = win_path.ok_or_else(|| "Windows 项目未配置 win_path".to_string())?;
        Ok(std::path::PathBuf::from(win))
    }
}

/// 项目存在性检查（替代旧的组归属校验——单一服务器寻址后组不再是访问边界）
fn ensure_project_exists(conn: &rusqlite::Connection, project_id: &str) -> Result<(), String> {
    let exists: bool = conn
        .query_row(
            "SELECT COUNT(*) FROM project_workspaces WHERE id = ?1",
            [project_id],
            |r| r.get::<_, i64>(0),
        )
        .map(|n| n > 0)
        .unwrap_or(false);
    if !exists {
        return Err(format!("项目 {} 不存在", project_id));
    }
    Ok(())
}

fn get_project_mentor_context(project_id: &str) -> Result<String, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    ensure_project_exists(&conn, project_id)?;
    let ctx = crate::commands::mentor::get_mentor_context(project_id.to_string())
        .map_err(|e| format!("获取导师上下文失败: {e}"))?;
    Ok(ctx.mentor_markdown)
}

fn append_project_note(project_id: &str, content: &str) -> Result<String, String> {
    if content.trim().is_empty() {
        return Err("笔记内容不能为空".to_string());
    }
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    ensure_project_exists(&conn, project_id)?;
    conn.execute(
        "INSERT INTO project_notes (project_id, source, content) VALUES (?1, 'mcp', ?2)",
        rusqlite::params![project_id, content],
    )
    .map_err(|e| e.to_string())?;
    Ok("笔记已保存 ✅".to_string())
}

fn get_project_snapshots(project_id: &str, days: u32) -> Result<String, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    ensure_project_exists(&conn, project_id)?;

    let mut stmt = conn
        .prepare(
            "SELECT snapshot_date, days_since_commit, blueprint_done_rate,
                    blueprint_in_progress, blueprint_blocked, has_errors
             FROM project_snapshots
             WHERE project_id = ?1
               AND snapshot_date >= date('now', 'localtime', ?2)
             ORDER BY snapshot_date ASC",
        )
        .map_err(|e| e.to_string())?;

    let offset = format!("-{} days", days);
    let rows: Vec<(String, Option<i64>, Option<f64>, i64, i64, i64)> = stmt
        .query_map(rusqlite::params![project_id, offset], |r| {
            Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?, r.get(5)?))
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    if rows.is_empty() {
        return Ok(format!(
            "项目 `{}` 暂无历史快照（快照在每次应用启动时写入，最多保留 {} 天）。",
            project_id, days
        ));
    }

    let mut md = format!(
        "# 项目历史快照：`{}`\n\n最近 {} 天，共 {} 条记录\n\n",
        project_id,
        days,
        rows.len()
    );
    md.push_str("| 日期 | 距上次提交(天) | 蓝图完成率 | 进行中模块 | 阻塞任务 | 有错误 |\n");
    md.push_str("|------|--------------|-----------|-----------|---------|------|\n");
    for (date, days_commit, done_rate, in_prog, blocked, has_err) in &rows {
        let commit_col = days_commit.map(|d| d.to_string()).unwrap_or_else(|| "—".to_string());
        let rate_col = done_rate
            .map(|r| format!("{:.0}%", r * 100.0))
            .unwrap_or_else(|| "—".to_string());
        let err_col = if *has_err == 1 { "⚠️" } else { "✅" };
        md.push_str(&format!(
            "| {} | {} | {} | {} | {} | {} |\n",
            date, commit_col, rate_col, in_prog, blocked, err_col
        ));
    }
    md.push_str("\n---\n\n请根据以上趋势数据，分析哪些变量与项目停滞或健康度下降最相关，并给出改进建议。\n");
    Ok(md)
}

pub fn list_all_projects() -> Result<String, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    let mut stmt = conn
        .prepare(
            "SELECT pw.id, pp.name, pw.platform, pw.win_path, pw.wsl_path, pgm.group_id
             FROM project_workspaces pw
             JOIN project_profiles pp ON pp.id = pw.profile_id
             LEFT JOIN project_group_map pgm ON pgm.project_id = pw.id
             ORDER BY pp.name",
        )
        .map_err(|e| e.to_string())?;

    let rows: Vec<(String, String, Option<String>, Option<String>, Option<String>, Option<String>)> =
        stmt.query_map([], |r| {
            Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?, r.get(5)?))
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    if rows.is_empty() {
        return Ok("炼境暂无登记项目。".to_string());
    }

    let mut out = format!("## 炼境全量项目（共 {} 个）\n\n", rows.len());
    for (id, name, platform, win_path, wsl_path, group_id) in &rows {
        let project_path = if platform.as_deref() == Some("wsl") {
            wsl_path.as_ref().map(|p| {
                let conn2 = db::pool().get().ok();
                let distro = conn2
                    .and_then(|c| {
                        c.query_row(
                            "SELECT value FROM settings WHERE key = 'wsl_distro'",
                            [],
                            |r| r.get::<_, String>(0),
                        )
                        .ok()
                    })
                    .unwrap_or_else(|| "Ubuntu".to_string());
                let inner = p.trim_start_matches('/').replace('/', "\\");
                format!("\\\\wsl.localhost\\{}\\{}", distro, inner)
            })
        } else {
            win_path.clone()
        };

        let injected = project_path
            .as_ref()
            .map(|p| std::path::Path::new(p).join(".claude").join("liangjing.json").exists())
            .unwrap_or(false);

        let inject_icon = if injected { "✅ 已注入" } else { "⚠️  未注入" };
        let group_str = group_id.as_deref().unwrap_or("无产品组");
        let path_str = project_path.as_deref().unwrap_or("未配置路径");
        out.push_str(&format!(
            "- **{}** `{}`\n  {} | 组: `{}` | {}\n\n",
            name, id, inject_icon, group_str, path_str
        ));
    }
    out.push_str("---\n对未注入项目调用 `inject_mcp(project_id)` 完成接入。");
    Ok(out)
}

pub fn scan_unregistered_projects() -> Result<String, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;

    // 取所有已登记的 win_path
    let mut stmt = conn
        .prepare("SELECT win_path FROM project_workspaces WHERE win_path IS NOT NULL")
        .map_err(|e| e.to_string())?;
    let registered_paths: std::collections::HashSet<String> = stmt
        .query_map([], |r| r.get::<_, String>(0))
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .map(|p| p.trim_end_matches(['/', '\\']).to_string())
        .collect();

    // 推导父目录（去重）
    let mut parent_dirs: std::collections::HashSet<std::path::PathBuf> =
        std::collections::HashSet::new();
    for p in &registered_paths {
        if let Some(parent) = std::path::Path::new(p).parent() {
            parent_dirs.insert(parent.to_path_buf());
        }
    }

    if parent_dirs.is_empty() {
        return Ok("炼境暂无登记项目，无法推导扫描目录。".to_string());
    }

    let mut unregistered: Vec<(String, bool, Vec<String>, bool)> = Vec::new();

    for parent in &parent_dirs {
        let entries = std::fs::read_dir(parent).map_err(|e| e.to_string())?;
        for entry in entries.flatten() {
            let path = entry.path();
            if !path.is_dir() {
                continue;
            }
            let path_str = path.to_string_lossy().trim_end_matches(['/', '\\']).to_string();
            if registered_paths.contains(&path_str) {
                continue;
            }
            // 隐藏目录跳过
            if path
                .file_name()
                .and_then(|n| n.to_str())
                .map(|n| n.starts_with('.'))
                .unwrap_or(false)
            {
                continue;
            }

            let has_git = path.join(".git").exists();
            let mut stack: Vec<String> = Vec::new();
            if path.join("package.json").exists() {
                stack.push("JS/TS".into());
            }
            if path.join("Cargo.toml").exists() {
                stack.push("Rust".into());
            }
            if path.join("go.mod").exists() {
                stack.push("Go".into());
            }
            if path.join("pom.xml").exists() {
                stack.push("Java".into());
            }
            let has_agents = path.join("AGENTS.md").exists();

            unregistered.push((path_str, has_git, stack, has_agents));
        }
    }

    if unregistered.is_empty() {
        return Ok(format!(
            "扫描了 {} 个父目录，所有子目录均已在炼境注册。",
            parent_dirs.len()
        ));
    }

    unregistered.sort_by(|a, b| a.0.cmp(&b.0));

    let mut out = format!(
        "## 未注册目录（共 {} 个）\n\n扫描范围：{}\n\n",
        unregistered.len(),
        parent_dirs
            .iter()
            .map(|p| p.to_string_lossy().to_string())
            .collect::<Vec<_>>()
            .join("、")
    );

    for (path, has_git, stack, has_agents) in &unregistered {
        let git_icon = if *has_git { "🗂 git" } else { "❌ 无git" };
        let agents_icon = if *has_agents { "📄 AGENTS" } else { "—" };
        let stack_str = if stack.is_empty() {
            "未知".to_string()
        } else {
            stack.join("/")
        };
        let name = std::path::Path::new(path)
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_else(|| path.clone());
        out.push_str(&format!(
            "- **{}** | {} | {} | {}\n  `{}`\n\n",
            name, git_icon, stack_str, agents_icon, path
        ));
    }

    out.push_str("---\n在炼境 UI 添加项目后，调用 `apply_onboarding_pack(project_id)` 分发接入包。");
    Ok(out)
}

// ── 测试 ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tools_list_contains_expected_tools() {
        let result = tools_list_result();
        let tools = result["tools"].as_array().expect("tools 应为数组");
        let names: Vec<&str> = tools
            .iter()
            .filter_map(|t| t["name"].as_str())
            .collect();
        // 核心工具必须存在
        assert!(names.contains(&"apply_onboarding_pack"), "缺少 apply_onboarding_pack");
        assert!(names.contains(&"verify_onboarding"), "缺少 verify_onboarding");
        assert!(names.contains(&"list_all_projects"), "缺少 list_all_projects");
        assert!(
            names.contains(&"scan_unregistered_projects"),
            "缺少 scan_unregistered_projects"
        );
        assert!(names.contains(&"inject_mcp"), "缺少 inject_mcp");
        assert!(names.contains(&"create_pull_request"), "缺少 create_pull_request");
        assert!(names.contains(&"poll_now"), "缺少 poll_now");
        assert!(names.contains(&"predict_task_risk"), "缺少 predict_task_risk");
        assert!(names.contains(&"ingest_git_history"), "缺少 ingest_git_history");
        assert!(names.contains(&"unregister_project"), "缺少 unregister_project");
        assert!(names.contains(&"setup_gitea_repo"), "缺少 setup_gitea_repo");
        // 每个工具都有 description 和 inputSchema
        for tool in tools {
            let name = tool["name"].as_str().unwrap_or("?");
            assert!(tool["description"].is_string(), "工具 {name} 缺少 description");
            assert!(tool["inputSchema"].is_object(), "工具 {name} 缺少 inputSchema");
        }
    }

    #[test]
    fn apply_onboarding_pack_schema_requires_project_id() {
        let result = tools_list_result();
        let tools = result["tools"].as_array().unwrap();
        for name in ["apply_onboarding_pack", "verify_onboarding"] {
            let tool = tools.iter().find(|t| t["name"] == name).unwrap();
            let required = tool["inputSchema"]["required"].as_array().unwrap();
            let fields: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
            assert!(fields.contains(&"project_id"), "{name} 应必填 project_id");
        }
    }
}
