//! 多 Agent 协作调度中心 — Coordinator
//!
//! Phase 1 实现，Phase 2 LLM 扩展
//!
//! 职责拆解（4 步）：
//! 1. **decompose**：接收用户意图 + 消息 → 拆解为 SubTask 列表
//! 2. **分配**：为每个 SubTask 分配人设（PersonaRegistry.recommend_for_intent）
//! 3. **dispatch**：并行执行 SubTask（当前 Phase 1 串行，预留 JoinSet 并行）
//! 4. **merge**：汇总子结果 → 合并产出 → 处理冲突

use crate::persona::PersonaRegistry;
use crate::planner::{Plan, SubTask, ValidateOptions};
use crate::provider::{ChatMessage, CompletionRequest, LlmProvider};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;

// ---- Plan-driven LLM 规划开关(Phase 1) -------------------------------------

/// aichat Plan-driven Phase 1 总开关(LLM 规划端)。
///
/// 默认 **关**(gradual 灰度,对齐 memory `ai-improvement-principles`「每改进配开关 +
/// 默认关 + 兜底可回退」)。开启时 `decompose_with_llm` 在 agentic loop 入口被调用,
/// 由 LLM 生成 Plan JSON(替代 `decompose` 关键词匹配)。
///
/// 与 `plan_executor::PLAN_EXECUTION_ENABLED`(Plan 执行端开关)正交:
/// - 本开关治「Plan 从哪来」(LLM 出 Plan);
/// - 执行端开关治「Plan 怎么执行」(JoinSet 并行 / 串行)。
///
/// **关时零行为变更**:agentic loop 入口走 `decompose`(关键词匹配)旧行为,
/// ReAct 主链不受影响。
static AICHAT_PLAN_ENABLED: AtomicBool = AtomicBool::new(false);

/// 设置 aichat Plan-driven 规划开关(运行时热切换,IPC / 前端可调)。
pub fn set_aichat_plan_enabled(enabled: bool) {
    AICHAT_PLAN_ENABLED.store(enabled, Ordering::SeqCst);
    tracing::info!(enabled, "[PLAN-LLM] aichat Plan-driven 规划开关已更新");
}

/// 读取 aichat Plan-driven 规划开关。
pub fn aichat_plan_enabled() -> bool {
    AICHAT_PLAN_ENABLED.load(Ordering::SeqCst)
}

// ---- Token 预算池 ------------------------------------------------------------

/// 全局 Token 预算池(CAS 无锁并发安全)
/// 多个 SubTask 启动前向预算池申请估算额度,超限时降级串行(不拒绝执行)。
#[derive(Debug)]
pub struct TokenBudgetPool {
    total: AtomicU64,
    consumed: AtomicU64,
}

impl TokenBudgetPool {
    /// 创建预算池。total=0 表示不限制(等价无限)。
    pub fn new(total: u64) -> Arc<Self> {
        Arc::new(Self {
            total: AtomicU64::new(total),
            consumed: AtomicU64::new(0),
        })
    }

    /// 尝试预占额度。成功返回 true,超限返回 false。
    /// total=0 时不限制,始终返回 true。
    pub fn try_reserve(&self, estimate: u64) -> bool {
        let total = self.total.load(Ordering::SeqCst);
        if total == 0 {
            return true;
        }
        let mut consumed = self.consumed.load(Ordering::SeqCst);
        loop {
            if consumed + estimate > total {
                return false;
            }
            match self.consumed.compare_exchange_weak(
                consumed,
                consumed + estimate,
                Ordering::SeqCst,
                Ordering::SeqCst,
            ) {
                Ok(_) => return true,
                Err(actual) => consumed = actual,
            }
        }
    }

    /// 已消耗额度
    pub fn consumed(&self) -> u64 {
        self.consumed.load(Ordering::SeqCst)
    }

    /// 总预算(0=不限制)
    pub fn total(&self) -> u64 {
        self.total.load(Ordering::SeqCst)
    }
}

// ---- 枚举 --------------------------------------------------------------------

/// 调度策略枚举
pub enum DispatchStrategy {
    /// 规则驱动（关键词匹配，Phase 1 MVP）
    RuleBased,
    /// LLM 驱动（LLM 生成 Plan，Phase 2 预留）
    LLMDriven,
}

impl DispatchStrategy {
    /// 策略名称标签（日志 / 审计用）
    pub fn name(&self) -> &'static str {
        match self {
            Self::RuleBased => "rule_based",
            Self::LLMDriven => "llm_driven",
        }
    }
}

// ---- 数据结构 ----------------------------------------------------------------

/// 拆解结果
pub struct DecompositionResult {
    /// 拆解出的子任务列表
    pub subtasks: Vec<SubTask>,
    /// 对应 Plan 结构（含依赖关系）
    pub plan: Plan,
}

/// 执行结果
pub struct ExecutionResult {
    /// 对应 SubTask id
    pub subtask_id: String,
    /// 分配的人设 id
    pub persona_id: String,
    /// 执行产出（工具结果摘要 / LLM 回复）
    pub output: String,
    /// 执行是否成功
    pub success: bool,
}

/// 合并结果
pub struct MergeResult {
    /// 合并后的最终产出
    pub merged_output: String,
    /// 检测到的冲突列表（Phase 2 由 reviewer Agent 填充）
    pub conflicts: Vec<ConflictItem>,
}

/// 冲突项
pub struct ConflictItem {
    /// 冲突涉及的文件
    pub file: String,
    /// 冲突描述
    pub description: String,
    /// 冲突双方 SubTask id
    pub subtask_a: Option<String>,
    pub subtask_b: Option<String>,
    /// Reviewer 仲裁推荐(Phase 2 LLM 仲裁填充)
    pub recommendation: Option<ConflictResolution>,
}

/// 冲突解决方案
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConflictResolution {
    /// 接受 A 的改动
    AcceptA,
    /// 接受 B 的改动
    AcceptB,
    /// 合并两者
    Merged,
    /// 用户手动处理
    Manual,
}

impl ConflictResolution {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::AcceptA => "a",
            Self::AcceptB => "b",
            Self::Merged => "merged",
            Self::Manual => "manual",
        }
    }
}

// ---- Coordinator 实现 --------------------------------------------------------

/// 多 Agent 协作调度器
///
/// 当前实现:
/// - 规则驱动 decompose(关键词匹配)
/// - dispatch_with_budget: JoinSet 层内并行 + token 预算管控
/// - dispatch_serial: 串行降级路径(无 'static 约束)
/// - merge: 拼接产出 + 同文件冲突检测
///
/// Phase 2 预留:
/// - LLM 驱动 decompose
/// - Reviewer Agent 仲裁
pub struct Coordinator {
    /// 人设注册表（内置 5 人设 + 自定义）
    registry: PersonaRegistry,
}

impl Coordinator {
    /// 创建 Coordinator，绑定人设注册表
    pub fn new(registry: PersonaRegistry) -> Self {
        Self { registry }
    }

    /// 规则拆解：intent + text → SubTask 列表 + Plan
    ///
    /// Phase 1 规则（关键词匹配）：
    /// - 检测 text 中是否含 `read`/`写`/`查看`/`分析`/`search` 等关键词 → 读子任务
    /// - 检测 text 中是否含 `write`/`修改`/`生成`/`create`/`fix` 等关键词 → 写子任务
    /// - 同时含读+写关键词 → 两个子任务，写依赖读（read → write）
    /// - 每 SubTask 分配人设（`PersonaRegistry::recommend_for_intent`）
    /// - 兜底：无法匹配任何关键词时产单个 default 子任务
    ///
    /// ## Phase 2 扩展点
    /// `DispatchStrategy::LLMDriven` 时改为调 LLM 生成 Plan 整体结构，
    /// 本方法当前仅走 `RuleBased` 路径。
    pub fn decompose(&self, intent: &str, text: &str) -> DecompositionResult {
        let lower = text.to_lowercase();

        // 规则 1: 读操作关键词
        let has_read = lower.contains("read")
            || lower.contains("查看")
            || lower.contains("读取")
            || lower.contains("分析")
            || lower.contains("search")
            || lower.contains("搜索")
            || lower.contains("审查")
            || lower.contains("review");

        // 规则 2: 写操作关键词
        let has_write = lower.contains("write")
            || lower.contains("写")
            || lower.contains("修改")
            || lower.contains("生成")
            || lower.contains("创建")
            || lower.contains("implement")
            || lower.contains("fix")
            || lower.contains("修复")
            || lower.contains("新增");

        let mut subtasks = Vec::new();

        match (has_read, has_write) {
            (true, true) => {
                // 读 + 写 → 两个子任务：read → write
                subtasks.push(
                    SubTask::new("read", "读取/分析现有代码")
                        .with_tools(vec![
                            "read_file".into(),
                            "search_files".into(),
                            "list_directory".into(),
                        ])
                        .with_deps(vec![]),
                );
                subtasks.push(
                    SubTask::new("write", "修改/生成代码")
                        .with_tools(vec![
                            "write_file".into(),
                            "patch_file".into(),
                            "edit_file".into(),
                        ])
                        .with_deps(vec!["read".into()]),
                );
            }
            (true, false) => {
                // 纯读 → 单个读子任务
                subtasks.push(
                    SubTask::new("read", "读取/分析代码")
                        .with_tools(vec![
                            "read_file".into(),
                            "search_files".into(),
                            "list_directory".into(),
                        ])
                        .with_deps(vec![]),
                );
            }
            (false, true) => {
                // 纯写 → 单个写子任务
                subtasks.push(
                    SubTask::new("write", "修改/生成代码")
                        .with_tools(vec![
                            "write_file".into(),
                            "patch_file".into(),
                            "edit_file".into(),
                        ])
                        .with_deps(vec![]),
                );
            }
            (false, false) => {
                // 兜底：按 intent 分配单个默认子任务
                subtasks.push(
                    SubTask::new("default", intent.to_string())
                        .with_tools(vec![])
                        .with_deps(vec![]),
                );
            }
        }

        let plan = Plan::from_tasks(subtasks.clone());
        DecompositionResult { subtasks, plan }
    }

    /// 推荐人设 id(供 agentic loop 构建 AiPlanCreated 事件载荷时映射 persona_id)。
    ///
    /// 暴露 registry.recommend_for_intent,使外部(无需自行持有 PersonaRegistry)
    /// 能把 SubTask.intent → persona_id 映射填充到 SubTaskInfo.persona_id。
    pub fn recommend_persona_id(&self, intent: &str) -> Option<String> {
        Some(self.registry.recommend_for_intent(intent).id.clone())
    }

    /// LLM 驱动拆解(Plan-driven Phase 1):intent + text → LLM 出 Plan JSON → Plan。
    ///
    /// 替代 [`Self::decompose`] 的关键词匹配——LLM 在 system prompt 引导下出
    /// 「步骤数组,每步含 tool_hint + risk + deps」的结构化 JSON,经 serde 解析成
    /// [`Plan`] 后用 [`Plan::validate_with`] 兜底校验。
    ///
    /// ## 参数
    /// - `provider`:LLM Provider(`&dyn LlmProvider`,调用方经 build_provider_for 构造)
    /// - `model`:模型 id(`select_model_id` 路由结果 / 兜底 default_model)
    /// - `intent`:意图标签(intent.rs IntentRecognizer 推断,作上下文提示)
    /// - `text`:用户原始消息(规划素材,末条 active user 消息)
    /// - `available_tools`:可用工具名清单(喂给 LLM 限定 tool_hint 取值域,防幻觉工具名)
    ///
    /// ## 返回值
    /// - `Ok(Some(result))`:LLM 出 Plan 且 validate 通过 → 走 Plan 路径
    /// - `Ok(None)`:LLM 调用失败 / JSON 解析失败 / validate 失败 → **回退纯 ReAct**
    ///   (调用方据 None 不进 Plan 分支,继续单链 ReAct,不阻断主流程)
    ///
    /// ## 兜底(对齐 memory `ai-improvement-principles`「每改进配兜底 + 可回退」)
    /// 三重兜底:provider.complete 失败 / serde 解析失败 / validate 失败 → 均 `Ok(None)`。
    /// 调用方 agentic loop 收 None 后不阻断,继续走 ReAct 主链(零回归)。
    pub async fn decompose_with_llm(
        &self,
        provider: &dyn LlmProvider,
        model: &str,
        intent: &str,
        text: &str,
        available_tools: &[String],
    ) -> Option<DecompositionResult> {
        // 1) 构造 system prompt + user prompt,调 LLM 出 Plan JSON
        let system_prompt = plan_llm_system_prompt(available_tools);
        let user_prompt = format!(
            "用户意图标签: {}\n\n用户消息:\n{}\n\n请输出执行计划 JSON。",
            intent, text
        );
        let request = CompletionRequest {
            model: model.to_string(),
            messages: vec![
                ChatMessage::system(system_prompt),
                ChatMessage::user(user_prompt),
            ],
            temperature: Some(0.3),
            max_tokens: Some(2048),
            stream: false,
            tools: None,
            tool_choice: None,
            reasoning_content: None,
        };

        // 2) 调 LLM(无超时:provider.complete 自身语义,调用方可包 tokio::time::timeout)
        let resp = match provider.complete(request).await {
            Ok(r) => r,
            Err(e) => {
                tracing::warn!(
                    intent = intent,
                    "[PLAN-LLM] LLM 调用失败,回退纯 ReAct: {}",
                    e
                );
                return None;
            }
        };

        // 3) 解析 JSON(允许 LLM 包 markdown 代码围栏 / 前后杂文本)
        let plan_json: PlanLlmOutput = match parse_plan_json(&resp.text) {
            Some(p) => p,
            None => {
                tracing::warn!(
                    intent = intent,
                    text_preview = %resp.text.chars().take(200).collect::<String>(),
                    "[PLAN-LLM] JSON 解析失败,回退纯 ReAct"
                );
                return None;
            }
        };

        // 4) 转 SubTask/Plan + validate 兜底
        //    require_tools=false:LLM 可能产「思考/协调」类无工具步骤(纯编排节点),
        //    关 require_tools 避免误拒(对齐 plan_hint 场景允许无工具子任务)。
        let subtasks: Vec<SubTask> = plan_json.into_subtasks();
        if subtasks.is_empty() {
            tracing::warn!("[PLAN-LLM] LLM 返回空步骤列表,回退纯 ReAct");
            return None;
        }
        let plan = Plan::from_tasks(subtasks.clone());
        let opts = ValidateOptions {
            require_tools: false,
            max_depth: crate::planner::MAX_PLAN_DEPTH,
        };
        let errs = plan.validate_with(opts);
        if !errs.is_empty() {
            tracing::warn!(
                task_count = plan.tasks.len(),
                errors = ?errs,
                "[PLAN-LLM] Plan validate 失败,回退纯 ReAct"
            );
            return None;
        }

        tracing::info!(
            intent = intent,
            task_count = subtasks.len(),
            "[PLAN-LLM] LLM 规划成功"
        );
        Some(DecompositionResult {
            subtasks,
            plan,
        })
    }

    /// 分发执行:按 Plan 分层执行 SubTask(层间串行 + 层内并行)
    ///
    /// - 层间串行:上层全部 done 才进下一层(DAG 依赖保证)
    /// - 层内并行:同层 SubTask 用 tokio::task::JoinSet 并发执行
    /// - Token 预算超限时降级为串行(不拒绝执行)
    ///
    /// ## 参数
    /// - `plan`: 待执行的 DAG Plan
    /// - `executor`: 子任务执行函数(接收 SubTask + persona_id 字符串)
    /// - `budget`: Token 预算池(None=不限制)
    ///
    /// ## 返回值
    /// 按 Plan 原始 tasks 顺序排列的执行结果列表。
    pub async fn dispatch_with_budget<F, Fut>(
        &self,
        plan: &Plan,
        executor: F,
        budget: Option<&Arc<TokenBudgetPool>>,
    ) -> Vec<ExecutionResult>
    where
        // 'static + Clone 要求:JoinSet::spawn 需要 owned 闭包(不能借用 self/db)。
        // 调用方需用 Arc 包裹共享状态(db/session)传入闭包。
        // 不需要并行时用 dispatch_serial(无 'static 约束)。
        F: Fn(SubTask, String) -> Fut + Clone + Send + Sync + 'static,
        Fut: std::future::Future<Output = ExecutionResult> + Send,
    {
        let layers = match plan.to_layers() {
            Ok(l) => l,
            Err(_) => {
                tracing::warn!("[COORDINATOR] DAG 分层失败,降级串行");
                let mut results = Vec::new();
                for task in &plan.tasks {
                    let persona_id = self.registry.recommend_for_intent(&task.intent).id.clone();
                    let result = executor(task.clone(), persona_id).await;
                    results.push(result);
                }
                return results;
            }
        };

        // 预分配每个 SubTask 的 persona_id(避免在 JoinSet 内借用 self)
        let task_persona: std::collections::HashMap<String, String> = plan
            .tasks
            .iter()
            .map(|t| {
                (t.id.clone(), self.registry.recommend_for_intent(&t.intent).id.clone())
            })
            .collect();

        let mut results_map: std::collections::HashMap<String, ExecutionResult> =
            std::collections::HashMap::new();

        for (layer_idx, layer) in layers.iter().enumerate() {
            if layer.len() <= 1 {
                for task in layer {
                    let pid = task_persona.get(&task.id).cloned().unwrap_or_default();
                    let result = executor(task.clone(), pid).await;
                    results_map.insert(result.subtask_id.clone(), result);
                }
                continue;
            }

            let can_parallel = budget
                .map(|b| b.try_reserve(layer.len() as u64 * 10_000)) // 每层按 SubTask 数累加
                .unwrap_or(true);

            if can_parallel {
                tracing::info!(
                    layer = layer_idx,
                    count = layer.len(),
                    "[COORDINATOR] 层 {} 并行执行 {} 个子任务",
                    layer_idx,
                    layer.len()
                );

                let mut join_set: tokio::task::JoinSet<ExecutionResult> =
                    tokio::task::JoinSet::new();

                for task in layer {
                    let pid = task_persona.get(&task.id).cloned().unwrap_or_default();
                    let exec = executor.clone();
                    let task = task.clone();
                    join_set.spawn(async move {
                        exec(task, pid).await
                    });
                }

                while let Some(join_result) = join_set.join_next().await {
                    match join_result {
                        Ok(result) => {
                            results_map.insert(result.subtask_id.clone(), result);
                        }
                        Err(e) => {
                            tracing::error!("[COORDINATOR] 子任务 panic: {}", e);
                        }
                    }
                }
            } else {
                tracing::info!(
                    layer = layer_idx,
                    "[COORDINATOR] Token 预算超限,层 {} 降级串行", layer_idx
                );
                for task in layer {
                    let pid = task_persona.get(&task.id).cloned().unwrap_or_default();
                    let result = executor(task.clone(), pid).await;
                    results_map.insert(result.subtask_id.clone(), result);
                }
            }
        }

        plan.tasks
            .iter()
            .filter_map(|t| results_map.remove(&t.id))
            .collect()
    }

    /// 串行 dispatch(降级/Phase 1 兼容路径)
    pub async fn dispatch<F, Fut>(&self, plan: &Plan, executor: F) -> Vec<ExecutionResult>
    where
        F: Fn(SubTask, crate::persona::AgentPersona) -> Fut,
        Fut: std::future::Future<Output = ExecutionResult>,
    {
        self.dispatch_serial(plan, &executor).await
    }

    /// 内部串行执行(无并行)
    async fn dispatch_serial<F, Fut>(&self, plan: &Plan, executor: &F) -> Vec<ExecutionResult>
    where
        F: Fn(SubTask, crate::persona::AgentPersona) -> Fut,
        Fut: std::future::Future<Output = ExecutionResult>,
    {
        let mut results = Vec::new();
        for task in &plan.tasks {
            let persona = self.registry.recommend_for_intent(&task.intent);
            let result = executor(task.clone(), persona.clone()).await;
            results.push(result);
        }
        results
    }

    /// 合并:汇总子结果 → 合并产出 + 冲突检测
    ///
    /// - 成功的 SubTask 产出拼接为 merged_output
    /// - 失败的 SubTask 跳过(不参与合并)
    /// - 冲突检测:多个 SubTask 写同一文件路径 → 标记冲突
    pub fn merge(&self, results: &[ExecutionResult]) -> MergeResult {
        let success_results: Vec<&ExecutionResult> =
            results.iter().filter(|r| r.success).collect();

        let parts: Vec<String> = success_results
            .iter()
            .map(|r| {
                format!(
                    "## SubTask: {} (Persona: {})\n\n{}",
                    r.subtask_id, r.persona_id, r.output
                )
            })
            .collect();

        // 冲突检测:检查是否有多个 SubTask 改了同一文件
        let conflicts = self.detect_file_conflicts(results);

        MergeResult {
            merged_output: parts.join("\n\n---\n\n"),
            conflicts,
        }
    }

    /// 检测文件冲突:从 ExecutionResult 中提取写入的文件路径,同路径 → 冲突
    /// Phase 1:基于 output 文本中的文件路径关键词(简化检测)
    /// Phase 2:从 Git worktree 的 git diff 提取精确路径
    fn detect_file_conflicts(&self, results: &[ExecutionResult]) -> Vec<ConflictItem> {
        use std::collections::HashMap;

        // 收集每个 SubTask 写入的文件路径(从 output 中提取)
        let mut file_map: HashMap<String, Vec<String>> = HashMap::new();
        for r in results {
            if !r.success {
                continue;
            }
            let files = extract_written_files(&r.output);
            for f in files {
                file_map.entry(f).or_default().push(r.subtask_id.clone());
            }
        }

        // 同文件被多个 SubTask 写 → 冲突
        let mut conflicts = Vec::new();
        for (file, subtasks) in &file_map {
            if subtasks.len() > 1 {
                conflicts.push(ConflictItem {
                    file: file.clone(),
                    description: format!(
                        "文件 {} 被 {} 个子任务同时修改: {}",
                        file,
                        subtasks.len(),
                        subtasks.join(", ")
                    ),
                    subtask_a: subtasks.first().cloned(),
                    subtask_b: subtasks.get(1).cloned(),
                    recommendation: None,
                });
            }
        }
        conflicts
    }

    /// 仲裁冲突:对每个冲突给出推荐解决方案
    ///
    /// Phase 1:规则驱动(简单启发式)
    /// - 只有一方成功的冲突 → 推荐成功方
    /// - 两方都成功 → 推荐 Merged(需人工确认)
    /// - 无法判断 → Manual(留给用户)
    ///
    /// Phase 2 预留:LLM 驱动(读 diff → 推荐方案 + 理由)
    pub fn arbitrate_conflicts(
        &self,
        conflicts: &mut [ConflictItem],
        results: &[ExecutionResult],
    ) {
        let success_ids: std::collections::HashSet<&str> =
            results.iter().filter(|r| r.success).map(|r| r.subtask_id.as_str()).collect();

        for c in conflicts.iter_mut() {
            let a_ok = c.subtask_a.as_deref().map(|id| success_ids.contains(id)).unwrap_or(false);
            let b_ok = c.subtask_b.as_deref().map(|id| success_ids.contains(id)).unwrap_or(false);

            c.recommendation = Some(match (a_ok, b_ok) {
                (true, false) => ConflictResolution::AcceptA,
                (false, true) => ConflictResolution::AcceptB,
                (true, true) => ConflictResolution::Merged,
                (false, false) => ConflictResolution::Manual,
            });

            tracing::info!(
                file = %c.file,
                recommendation = ?c.recommendation,
                "[REVIEWER] 冲突仲裁推荐"
            );
        }
    }
}

/// 从执行产出文本中提取被写入的文件路径(简化检测:匹配 write_file/patch_file 后的路径)
fn extract_written_files(output: &str) -> Vec<String> {
    let mut files = Vec::new();
    for line in output.lines() {
        let trimmed = line.trim();
        // 匹配 "write_file: path" 或 "patch_file: path" 或 "写入: path" 模式
        for prefix in ["write_file:", "patch_file:", "写入:", "修改:"] {
            if let Some(rest) = trimmed.strip_prefix(prefix) {
                let path = rest.trim().split_whitespace().next().unwrap_or("");
                if !path.is_empty() {
                    files.push(path.to_string());
                }
            }
        }
    }
    files
}

// ---- Plan-driven LLM 规划辅助(Phase 1) --------------------------------------

/// LLM 输出的 Plan JSON 中间结构(serde 反序列化用)。
///
/// LLM 出形如:
/// ```json
/// { "steps": [
///   { "id": "read", "intent": "读取代码", "tools": ["read_file"], "deps": [] },
///   { "id": "write", "intent": "修改代码", "tools": ["patch_file"], "deps": ["read"] }
/// ] }
/// ```
/// `risk` 字段可选(LLM 可能省略,默认 "low");`group` 可选(并行组 hint)。
/// 字段命名走宽松容错:tools/deps 任一缺失均回退空 Vec(serde default)。
#[derive(Debug, serde::Deserialize)]
struct PlanLlmStep {
    /// 子任务 id(任务内唯一)。空或缺失 → 转换时按序号兜底生成。
    #[serde(default)]
    id: String,
    /// 意图描述(自由文本)。
    #[serde(default)]
    intent: String,
    /// 可用工具名子集(hint,非强制)。缺失 → 空 Vec。
    #[serde(default)]
    tools: Vec<String>,
    /// 依赖前驱 id 列表。缺失 → 空 Vec。
    #[serde(default)]
    deps: Vec<String>,
    /// 并行组 hint(可选)。缺失 → None。
    #[serde(default)]
    group: Option<String>,
}

/// Plan JSON 顶层结构:仅含 steps 数组。
#[derive(Debug, serde::Deserialize)]
struct PlanLlmOutput {
    #[serde(default)]
    steps: Vec<PlanLlmStep>,
}

impl PlanLlmOutput {
    /// 转换为 SubTask 列表(去空 id 兜底生成,去重 id 保留首个)。
    fn into_subtasks(self) -> Vec<SubTask> {
        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
        let mut out: Vec<SubTask> = Vec::new();
        for (idx, step) in self.steps.into_iter().enumerate() {
            // 空 id → 按 step_<idx> 兜底生成,避免 validate 拒 EmptyId
            let id = if step.id.trim().is_empty() {
                format!("step_{}", idx)
            } else {
                step.id.trim().to_string()
            };
            // 去重(validate 也会拒 DuplicateId,此处提前过滤防脏数据)
            if !seen.insert(id.clone()) {
                tracing::warn!(
                    dup_id = %id,
                    "[PLAN-LLM] 重复子任务 id,跳过(防 DuplicateId)"
                );
                continue;
            }
            out.push(SubTask {
                id,
                tool_hint: step.tools,
                deps: step.deps,
                group: step.group,
                intent: if step.intent.trim().is_empty() {
                    format!("step_{}", idx)
                } else {
                    step.intent
                },
            });
        }
        out
    }
}

/// Plan-driven LLM system prompt:引导 LLM 出结构化 Plan JSON。
///
/// 设计要点(对齐设计文档 §三 Plan 数据结构):
/// - 只输出 JSON(明确格式约定,防 LLM 输出杂文本)
/// - 工具名限定在 `available_tools` 集合内(防幻觉不存在的工具)
/// - deps 引用同 Plan 内的 id(防悬空)
/// - 单任务即可(不强制拆多步,简单问题不堆步骤)
/// - 风险高的步骤放后(顺序依赖自然表达)
fn plan_llm_system_prompt(available_tools: &[String]) -> String {
    // 工具名清单(逗号分隔,LLM 据此填 tool_hint)
    let tools_list = if available_tools.is_empty() {
        "(未提供工具清单,可留空)".to_string()
    } else {
        available_tools.join(", ")
    };
    format!(
        "你是执行计划规划器。根据用户的意图和消息,把任务拆解为**可执行的步骤**,\n\
         输出为严格的 JSON 格式(只输出 JSON,不要任何解释、markdown 围栏或前后文本)。\n\
         \n\
         输出格式:\n\
         ```\n\
         {{\n\
         \x20 \"steps\": [\n\
         \x20   {{\n\
         \x20     \"id\": \"唯一短标识(如 read/write/step1)\",\n\
         \x20     \"intent\": \"这步做什么(简短中文描述)\",\n\
         \x20     \"tools\": [\"工具名(从下方清单选)\"],\n\
         \x20     \"deps\": [\"依赖的前置步骤 id\"],\n\
         \x20     \"group\": \"可选,并行组标识\"\n\
         \x20   }}\n\
         \x20 ]\n\
         }}\n\
         ```\n\
         \n\
         可用工具清单: {tools_list}\n\
         \n\
         规则:\n\
         1. 只输出 JSON,首字符必须是 `{{`,末字符必须是 `}}`\n\
         2. steps 数组至少 1 个步骤(简单问题 1 个即可,不要为堆步骤而堆)\n\
         3. tools 从上方清单选,不存在的工具不要写\n\
         4. deps 只能引用同 Plan 内已定义的 id(不可悬空)\n\
         5. 风险高/有副作用的步骤(写文件/跑命令)放后面,依赖前置读步骤\n\
         6. 不要生成环依赖(A 依赖 B 且 B 依赖 A)"
    )
}

/// 从 LLM 输出文本中提取 Plan JSON 并反序列化。
///
/// 容错:LLM 可能(a)包 markdown 代码围栏(```json ... ```);(b)前后带杂文本;
/// (c)纯 JSON。统一处理:找到首个 `{` 到末个 `}` 的子串再 serde 解析。
/// 解析失败返 None(调用方回退 ReAct)。
fn parse_plan_json(raw: &str) -> Option<PlanLlmOutput> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return None;
    }
    // 剥离可能的整体 markdown 代码围栏
    let stripped = trimmed
        .strip_prefix("```json")
        .or_else(|| trimmed.strip_prefix("```"))
        .unwrap_or(trimmed)
        .trim_start_matches('\n');
    let stripped = stripped.strip_suffix("```").unwrap_or(stripped).trim();

    // 提取首个 { 到末个 } 的子串(防前后杂文本)
    let start = stripped.find('{')?;
    let end = stripped.rfind('}')?;
    if end <= start {
        return None;
    }
    let json_str = &stripped[start..=end];
    match serde_json::from_str::<PlanLlmOutput>(json_str) {
        Ok(p) => Some(p),
        Err(e) => {
            tracing::debug!(
                json_preview = %json_str.chars().take(200).collect::<String>(),
                error = %e,
                "[PLAN-LLM] serde 反序列化失败"
            );
            None
        }
    }
}

// ---- 单元测试 ---------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::persona::{PersonaRegistry, PERSONA_CODER};

    fn make_coord() -> Coordinator {
        Coordinator::new(PersonaRegistry::new())
    }

    // -- decompose 路径覆盖 --

    #[test]
    fn decompose_read_then_write_chinese() {
        let coord = make_coord();
        let result = coord.decompose("modify", "请读取当前代码，然后修改它");

        assert_eq!(result.subtasks.len(), 2, "读+写应拆为两个子任务");
        assert_eq!(result.subtasks[0].id, "read", "第一个子任务应为读");
        assert_eq!(result.subtasks[1].id, "write", "第二个子任务应为写");
        assert!(
            result.subtasks[1].deps.contains(&"read".to_string()),
            "写子任务应依赖读子任务"
        );
        assert_eq!(result.plan.len(), 2);
    }

    #[test]
    fn decompose_read_then_write_english() {
        let coord = make_coord();
        let result = coord.decompose("modify", "read the file and then write changes");

        assert_eq!(result.subtasks.len(), 2);
        assert_eq!(result.subtasks[0].id, "read");
        assert_eq!(result.subtasks[1].id, "write");
        assert!(result.subtasks[1].deps.contains(&"read".to_string()));
    }

    #[test]
    fn decompose_read_only_chinese() {
        let coord = make_coord();
        let result = coord.decompose("review", "帮我查看这段代码有什么问题");

        assert_eq!(result.subtasks.len(), 1);
        assert_eq!(result.subtasks[0].id, "read");
    }

    #[test]
    fn decompose_read_only_search() {
        let coord = make_coord();
        let result = coord.decompose("search", "搜索项目中的 TODO 注释");

        assert_eq!(result.subtasks.len(), 1);
        assert_eq!(result.subtasks[0].id, "read");
    }

    #[test]
    fn decompose_write_only_generate() {
        let coord = make_coord();
        let result = coord.decompose("implement", "请生成一个 Rust 模块");

        assert_eq!(result.subtasks.len(), 1);
        assert_eq!(result.subtasks[0].id, "write");
    }

    #[test]
    fn decompose_write_only_fix() {
        let coord = make_coord();
        let result = coord.decompose("fix", "修复这个 bug");

        assert_eq!(result.subtasks.len(), 1);
        assert_eq!(result.subtasks[0].id, "write");
    }

    #[test]
    fn decompose_fallback_default() {
        let coord = make_coord();
        let result = coord.decompose("chat", "你好，今天天气怎么样");

        assert_eq!(result.subtasks.len(), 1);
        assert_eq!(result.subtasks[0].id, "default");
    }

    #[test]
    fn decompose_create_keyword() {
        let coord = make_coord();
        let result = coord.decompose("create", "创建新的 API 端点");

        assert_eq!(result.subtasks.len(), 1);
        assert_eq!(result.subtasks[0].id, "write");
    }

    // -- dispatch --

    #[test]
    fn dispatch_serial_execution() {
        let coord = make_coord();
        let plan = Plan::from_tasks(vec![
            SubTask::new("read", "read code").with_deps(vec![]),
            SubTask::new("write", "write code").with_deps(vec!["read".into()]),
        ]);

        let results = futures::executor::block_on(coord.dispatch(&plan, |task, persona| {
            async move {
                ExecutionResult {
                    subtask_id: task.id,
                    persona_id: persona.id,
                    output: format!("执行 {} 完毕", task.intent),
                    success: true,
                }
            }
        }));

        assert_eq!(results.len(), 2);
        assert!(results[0].success);
        assert!(results[1].success);
        assert_eq!(results[0].subtask_id, "read");
        assert_eq!(results[1].subtask_id, "write");
        assert_eq!(results[0].persona_id, PERSONA_CODER);
    }

    #[test]
    fn dispatch_empty_plan() {
        let coord = make_coord();
        let plan = Plan::new();

        let results = futures::executor::block_on(coord.dispatch(&plan, |task, persona| {
            async move {
                ExecutionResult {
                    subtask_id: task.id,
                    persona_id: persona.id,
                    output: String::new(),
                    success: true,
                }
            }
        }));

        assert!(results.is_empty(), "空 Plan 应产生空结果");
    }

    #[test]
    fn dispatch_persona_assigned_by_intent() {
        let coord = make_coord();
        let plan = Plan::from_tasks(vec![
            SubTask::new("code", "implement feature").with_deps(vec![]),
        ]);

        let results = futures::executor::block_on(coord.dispatch(&plan, |task, persona| {
            async move {
                ExecutionResult {
                    subtask_id: task.id,
                    persona_id: persona.id,
                    output: String::new(),
                    success: true,
                }
            }
        }));

        // "implement" 不触发特殊人设 → 默认 coder
        assert_eq!(results[0].persona_id, PERSONA_CODER);
    }

    // -- merge --

    #[test]
    fn merge_single_result() {
        let coord = make_coord();
        let results = vec![ExecutionResult {
            subtask_id: "read".into(),
            persona_id: "reviewer".into(),
            output: "分析完成，发现 3 个问题".into(),
            success: true,
        }];

        let merged = coord.merge(&results);
        assert!(merged.merged_output.contains("分析完成"));
        assert!(merged.conflicts.is_empty(), "Phase 1 不应有冲突");
    }

    #[test]
    fn merge_multiple_results() {
        let coord = make_coord();
        let results = vec![
            ExecutionResult {
                subtask_id: "read".into(),
                persona_id: "reviewer".into(),
                output: "代码分析结果".into(),
                success: true,
            },
            ExecutionResult {
                subtask_id: "write".into(),
                persona_id: "coder".into(),
                output: "修改后的代码".into(),
                success: true,
            },
        ];

        let merged = coord.merge(&results);
        assert!(merged.merged_output.contains("代码分析结果"));
        assert!(merged.merged_output.contains("修改后的代码"));
        assert!(merged.merged_output.contains("SubTask: read"));
        assert!(merged.merged_output.contains("SubTask: write"));
        assert!(merged.conflicts.is_empty(), "Phase 1 不应有冲突");
    }

    #[test]
    fn merge_empty_results() {
        let coord = make_coord();
        let merged = coord.merge(&[]);
        assert!(merged.merged_output.is_empty(), "空输入应产生空输出");
        assert!(merged.conflicts.is_empty());
    }

    #[test]
    fn merge_skips_failed_results() {
        let coord = make_coord();
        let results = vec![
            ExecutionResult {
                subtask_id: "ok".into(),
                persona_id: "coder".into(),
                output: "成功产出".into(),
                success: true,
            },
            ExecutionResult {
                subtask_id: "fail".into(),
                persona_id: "coder".into(),
                output: "执行失败".into(),
                success: false,
            },
        ];
        let merged = coord.merge(&results);
        assert!(merged.merged_output.contains("成功产出"), "应含成功产出");
        assert!(!merged.merged_output.contains("执行失败"), "不应含失败产出");
    }

    #[test]
    fn merge_detects_same_file_conflict() {
        let coord = make_coord();
        let results = vec![
            ExecutionResult {
                subtask_id: "A".into(),
                persona_id: "coder".into(),
                output: "write_file: src/main.rs\n新增 auth 模块".into(),
                success: true,
            },
            ExecutionResult {
                subtask_id: "B".into(),
                persona_id: "coder".into(),
                output: "write_file: src/main.rs\n新增 payment 模块".into(),
                success: true,
            },
        ];
        let merged = coord.merge(&results);
        assert_eq!(merged.conflicts.len(), 1, "同文件应检测到 1 个冲突");
        assert!(merged.conflicts[0].file.contains("main.rs"));
        assert!(merged.conflicts[0].description.contains("A"));
        assert!(merged.conflicts[0].description.contains("B"));
    }

    #[test]
    fn merge_no_conflict_different_files() {
        let coord = make_coord();
        let results = vec![
            ExecutionResult {
                subtask_id: "A".into(),
                persona_id: "coder".into(),
                output: "write_file: src/auth.rs".into(),
                success: true,
            },
            ExecutionResult {
                subtask_id: "B".into(),
                persona_id: "coder".into(),
                output: "write_file: src/payment.rs".into(),
                success: true,
            },
        ];
        let merged = coord.merge(&results);
        assert!(merged.conflicts.is_empty(), "不同文件不应有冲突");
    }

    #[test]
    fn merge_conflict_ignored_for_failed() {
        let coord = make_coord();
        // 失败的 SubTask 不参与冲突检测
        let results = vec![
            ExecutionResult {
                subtask_id: "A".into(),
                persona_id: "coder".into(),
                output: "write_file: src/main.rs".into(),
                success: true,
            },
            ExecutionResult {
                subtask_id: "B".into(),
                persona_id: "coder".into(),
                output: "write_file: src/main.rs".into(),
                success: false, // 失败
            },
        ];
        let merged = coord.merge(&results);
        assert!(merged.conflicts.is_empty(), "失败的不应参与冲突检测");
    }

    #[test]
    fn extract_files_from_output() {
        let files = extract_written_files("write_file: src/main.rs\n其他内容");
        assert_eq!(files, vec!["src/main.rs"]);

        let files = extract_written_files("patch_file: lib/utils.rs\n修改完成");
        assert!(files.contains(&"lib/utils.rs".to_string()));

        let files = extract_written_files("没有写操作");
        assert!(files.is_empty());
    }

    // -- Reviewer 仲裁 --

    #[test]
    fn arb_01_both_success_recommends_merged() {
        let coord = make_coord();
        let mut conflicts = vec![ConflictItem {
            file: "main.rs".into(),
            description: "冲突".into(),
            subtask_a: Some("A".into()),
            subtask_b: Some("B".into()),
            recommendation: None,
        }];
        let results = vec![
            ExecutionResult { subtask_id: "A".into(), persona_id: "coder".into(), output: "".into(), success: true },
            ExecutionResult { subtask_id: "B".into(), persona_id: "coder".into(), output: "".into(), success: true },
        ];
        coord.arbitrate_conflicts(&mut conflicts, &results);
        assert_eq!(conflicts[0].recommendation, Some(ConflictResolution::Merged));
    }

    #[test]
    fn arb_02_a_failed_recommends_b() {
        let coord = make_coord();
        let mut conflicts = vec![ConflictItem {
            file: "main.rs".into(),
            description: "冲突".into(),
            subtask_a: Some("A".into()),
            subtask_b: Some("B".into()),
            recommendation: None,
        }];
        let results = vec![
            ExecutionResult { subtask_id: "A".into(), persona_id: "coder".into(), output: "".into(), success: false },
            ExecutionResult { subtask_id: "B".into(), persona_id: "coder".into(), output: "".into(), success: true },
        ];
        coord.arbitrate_conflicts(&mut conflicts, &results);
        assert_eq!(conflicts[0].recommendation, Some(ConflictResolution::AcceptB));
    }

    #[test]
    fn arb_03_both_failed_recommends_manual() {
        let coord = make_coord();
        let mut conflicts = vec![ConflictItem {
            file: "main.rs".into(),
            description: "冲突".into(),
            subtask_a: Some("A".into()),
            subtask_b: Some("B".into()),
            recommendation: None,
        }];
        let results = vec![
            ExecutionResult { subtask_id: "A".into(), persona_id: "coder".into(), output: "".into(), success: false },
            ExecutionResult { subtask_id: "B".into(), persona_id: "coder".into(), output: "".into(), success: false },
        ];
        coord.arbitrate_conflicts(&mut conflicts, &results);
        assert_eq!(conflicts[0].recommendation, Some(ConflictResolution::Manual));
    }

    // -- DispatchStrategy --

    #[test]
    fn dispatch_strategy_name() {
        assert_eq!(DispatchStrategy::RuleBased.name(), "rule_based");
        assert_eq!(DispatchStrategy::LLMDriven.name(), "llm_driven");
    }

    // -- Edge cases: 关键词混合边界 --

    #[test]
    fn decompose_review_triggers_read() {
        let coord = make_coord();
        // "review" 同时触发读关键词
        let result = coord.decompose("review", "review this PR for me");
        assert_eq!(result.subtasks.len(), 1);
        assert_eq!(result.subtasks[0].id, "read");
    }

    #[test]
    fn decompose_create_and_review_triggers_both() {
        let coord = make_coord();
        let result = coord.decompose("implement", "请先分析代码结构然后创建新模块");
        assert_eq!(
            result.subtasks.len(),
            2,
            "含分析和创建关键词应拆为两个子任务"
        );
    }

    #[test]
    fn decompose_plan_not_empty() {
        let coord = make_coord();
        let result = coord.decompose("chat", "随便聊聊");
        assert!(!result.plan.is_empty(), "兜底场景也应有 Plan");
    }

    #[test]
    fn decompose_result_holds_plan_consistent() {
        let coord = make_coord();
        let result = coord.decompose("modify", "读取配置文件然后更新它");
        assert_eq!(
            result.subtasks.len(),
            result.plan.len(),
            "DecompositionResult 的 subtasks 应与 plan 一致"
        );
        for st in &result.subtasks {
            assert!(
                result.plan.tasks.iter().any(|t| t.id == st.id),
                "plan 应包含所有 subtask: {}",
                st.id
            );
        }
    }

    // -- Token 预算池 --

    #[test]
    fn tok_01_budget_sufficient() {
        let pool = TokenBudgetPool::new(100_000);
        assert!(pool.try_reserve(10_000), "预算充足时应允许");
        assert_eq!(pool.consumed(), 10_000);
        assert!(pool.try_reserve(50_000), "剩余充足时应允许");
        assert_eq!(pool.consumed(), 60_000);
    }

    #[test]
    fn tok_02_budget_exceeded() {
        let pool = TokenBudgetPool::new(100_000);
        assert!(pool.try_reserve(98_000));
        assert!(!pool.try_reserve(5_000), "超限应拒绝");
        assert_eq!(pool.consumed(), 98_000, "拒绝后 consumed 不增");
    }

    #[test]
    fn tok_03_concurrent_reserve() {
        let pool = TokenBudgetPool::new(15_000);
        // 模拟两个并发申请各 10k,总额 20k > 15k,只有第一个应成功
        let pool1 = pool.clone();
        let pool2 = pool.clone();
        let r1 = pool1.try_reserve(10_000);
        let r2 = pool2.try_reserve(10_000);
        // 至少一个成功一个失败(顺序由调度决定)
        assert!(r1 || r2, "至少一个成功");
        assert!(!r1 || !r2, "不能两个都成功(总超预算)");
    }

    #[test]
    fn tok_04_zero_means_unlimited() {
        let pool = TokenBudgetPool::new(0);
        assert!(pool.try_reserve(999_999_999), "total=0 不限制");
    }

    // -- 并行调度 --

    #[tokio::test]
    async fn par_01_diamond_dependency() {
        // A → {B, C} → D,B/C 同层应并行,D 等 B+C 都 done
        let coord = make_coord();
        let plan = Plan::from_tasks(vec![
            SubTask::new("A", "base"),
            SubTask::new("B", "branch1").with_deps(vec!["A".into()]),
            SubTask::new("C", "branch2").with_deps(vec!["A".into()]),
            SubTask::new("D", "final").with_deps(vec!["B".into(), "C".into()]),
        ]);

        let results = coord.dispatch_with_budget(&plan, |task, persona_id: String| {
            async move {
                ExecutionResult {
                    subtask_id: task.id,
                    persona_id,
                    output: format!("executed {}", task.intent),
                    success: true,
                }
            }
        }, None).await;

        assert_eq!(results.len(), 4, "全部 4 个子任务应有结果");
        // 结果按 plan.tasks 原始顺序
        assert_eq!(results[0].subtask_id, "A");
        assert_eq!(results[1].subtask_id, "B");
        assert_eq!(results[2].subtask_id, "C");
        assert_eq!(results[3].subtask_id, "D");
    }

    #[tokio::test]
    async fn par_02_parallel_layer_executed() {
        // 3 个无依赖任务应在同层并行执行
        let coord = make_coord();
        let plan = Plan::from_tasks(vec![
            SubTask::new("t1", "read file1"),
            SubTask::new("t2", "read file2"),
            SubTask::new("t3", "read file3"),
        ]);

        let results = coord.dispatch_with_budget(&plan, |task, persona_id: String| {
            async move {
                ExecutionResult {
                    subtask_id: task.id.clone(),
                    persona_id,
                    output: format!("done {}", task.id),
                    success: true,
                }
            }
        }, None).await;

        assert_eq!(results.len(), 3);
        for r in &results {
            assert!(r.success, "所有子任务应成功");
        }
    }

    #[tokio::test]
    async fn par_03_budget_exceeded_degrades_to_serial() {
        // Token 预算仅够 1 个并行,第 2 个应降级串行(不拒绝)
        let coord = make_coord();
        let plan = Plan::from_tasks(vec![
            SubTask::new("t1", "task1"),
            SubTask::new("t2", "task2"),
            SubTask::new("t3", "task3"),
        ]);
        let pool = TokenBudgetPool::new(5_000); // 不够 3×10k

        let results = coord.dispatch_with_budget(&plan, |task, persona_id: String| {
            async move {
                ExecutionResult {
                    subtask_id: task.id,
                    persona_id,
                    output: String::new(),
                    success: true,
                }
            }
        }, Some(&pool)).await;

        // 预算超限应降级串行,不拒绝执行
        assert_eq!(results.len(), 3, "降级串行也应全部执行");
    }

    #[tokio::test]
    async fn par_04_single_task_layer_serial() {
        // 线性链(每层 1 任务)不走并行路径
        let coord = make_coord();
        let plan = Plan::from_tasks(vec![
            SubTask::new("a", "step1"),
            SubTask::new("b", "step2").with_deps(vec!["a".into()]),
        ]);

        let results = coord.dispatch_with_budget(&plan, |task, persona_id: String| {
            async move {
                ExecutionResult {
                    subtask_id: task.id,
                    persona_id,
                    output: String::new(),
                    success: true,
                }
            }
        }, None).await;

        assert_eq!(results.len(), 2);
        assert_eq!(results[0].subtask_id, "a");
        assert_eq!(results[1].subtask_id, "b");
    }

    // -- Plan-driven LLM 规划开关 --

    #[test]
    fn plan_llm_gate_default_off() {
        // 默认关:零回归(现有 ReAct 行为不变)
        // 注:静态 AtomicBool 在测试间共享状态,此处仅断言默认值语义(关)。
        // 不强测 set 后值(会污染其他测试的全局静态态),set/get 由 IPC 路径实测。
        assert!(!aichat_plan_enabled(), "AICHAT_PLAN_ENABLED 应默认关");
    }

    #[test]
    fn plan_llm_gate_set_get_roundtrip() {
        // 保存原值,set 后 get 应一致,最后恢复(防污染其他测试)
        let original = aichat_plan_enabled();
        set_aichat_plan_enabled(true);
        assert!(aichat_plan_enabled(), "set true 后 get 应为 true");
        set_aichat_plan_enabled(false);
        assert!(!aichat_plan_enabled(), "set false 后 get 应为 false");
        // 恢复(防测试间全局态污染)
        set_aichat_plan_enabled(original);
    }

    // -- plan_llm_system_prompt --

    #[test]
    fn plan_llm_system_prompt_lists_tools() {
        let prompt = plan_llm_system_prompt(&["read_file".into(), "write_file".into()]);
        assert!(prompt.contains("read_file"));
        assert!(prompt.contains("write_file"));
        assert!(prompt.contains("steps"));
        assert!(prompt.contains("JSON"));
    }

    #[test]
    fn plan_llm_system_prompt_empty_tools() {
        let prompt = plan_llm_system_prompt(&[]);
        // 空工具清单 → fallback 文案,不 panic
        assert!(prompt.contains("JSON"));
    }

    // -- parse_plan_json: 容错解析 --

    #[test]
    fn parse_plan_json_pure_json() {
        let raw = r#"{"steps":[{"id":"read","intent":"读","tools":["read_file"],"deps":[]}]}"#;
        let p = parse_plan_json(raw).expect("纯 JSON 应解析");
        assert_eq!(p.steps.len(), 1);
        assert_eq!(p.steps[0].id, "read");
        assert_eq!(p.steps[0].tools, vec!["read_file".to_string()]);
    }

    #[test]
    fn parse_plan_json_with_markdown_fence() {
        let raw = "```json\n{\"steps\":[{\"id\":\"a\",\"intent\":\"x\"}]}\n```";
        let p = parse_plan_json(raw).expect("带 ```json 围栏应解析");
        assert_eq!(p.steps.len(), 1);
        assert_eq!(p.steps[0].id, "a");
    }

    #[test]
    fn parse_plan_json_with_surrounding_text() {
        let raw = "好的,这是计划:\n{\"steps\":[{\"id\":\"a\"}]}\n以上是计划。";
        let p = parse_plan_json(raw).expect("前后杂文本应提取子串解析");
        assert_eq!(p.steps.len(), 1);
    }

    #[test]
    fn parse_plan_json_missing_optional_fields() {
        // 缺 tools/deps/group → serde default 兜底空 Vec/None
        let raw = r#"{"steps":[{"id":"a","intent":"do"}]}"#;
        let p = parse_plan_json(raw).expect("缺可选字段应解析");
        assert_eq!(p.steps[0].tools, Vec::<String>::new());
        assert_eq!(p.steps[0].deps, Vec::<String>::new());
        assert!(p.steps[0].group.is_none());
    }

    #[test]
    fn parse_plan_json_empty_returns_none() {
        assert!(parse_plan_json("").is_none());
        assert!(parse_plan_json("   ").is_none());
    }

    #[test]
    fn parse_plan_json_malformed_returns_none() {
        // 非法 JSON → None(不 panic)
        assert!(parse_plan_json("{not valid json}").is_none());
        assert!(parse_plan_json("no braces here").is_none());
    }

    #[test]
    fn parse_plan_json_empty_steps_array() {
        // 合法 JSON 但 steps 空 → 解析成功(steps 空 Vec),由 into_subtasks/validate 兜底
        let raw = r#"{"steps":[]}"#;
        let p = parse_plan_json(raw).expect("空 steps 数组合法 JSON 应解析");
        assert!(p.steps.is_empty());
    }

    // -- PlanLlmOutput::into_subtasks --

    #[test]
    fn into_subtasks_basic() {
        let p = PlanLlmOutput {
            steps: vec![
                PlanLlmStep {
                    id: "read".into(),
                    intent: "读代码".into(),
                    tools: vec!["read_file".into()],
                    deps: vec![],
                    group: None,
                },
                PlanLlmStep {
                    id: "write".into(),
                    intent: "写代码".into(),
                    tools: vec!["write_file".into()],
                    deps: vec!["read".into()],
                    group: None,
                },
            ],
        };
        let tasks = p.into_subtasks();
        assert_eq!(tasks.len(), 2);
        assert_eq!(tasks[0].id, "read");
        assert_eq!(tasks[1].deps, vec!["read".to_string()]);
    }

    #[test]
    fn into_subtasks_empty_id_gets_fallback() {
        let p = PlanLlmOutput {
            steps: vec![PlanLlmStep {
                id: "".into(),
                intent: "do".into(),
                tools: vec![],
                deps: vec![],
                group: None,
            }],
        };
        let tasks = p.into_subtasks();
        assert_eq!(tasks.len(), 1);
        assert_eq!(tasks[0].id, "step_0", "空 id 应兜底 step_<idx>");
    }

    #[test]
    fn into_subtasks_dedup_duplicate_id() {
        let p = PlanLlmOutput {
            steps: vec![
                PlanLlmStep { id: "a".into(), intent: "1".into(), tools: vec![], deps: vec![], group: None },
                PlanLlmStep { id: "a".into(), intent: "2".into(), tools: vec![], deps: vec![], group: None },
            ],
        };
        let tasks = p.into_subtasks();
        assert_eq!(tasks.len(), 1, "重复 id 应去重保留首个");
    }

    #[test]
    fn into_subtasks_empty_intent_gets_fallback() {
        let p = PlanLlmOutput {
            steps: vec![PlanLlmStep {
                id: "x".into(),
                intent: "".into(),
                tools: vec![],
                deps: vec![],
                group: None,
            }],
        };
        let tasks = p.into_subtasks();
        assert!(!tasks[0].intent.is_empty(), "空 intent 应兜底非空");
    }

    // -- decompose_with_llm: 用 mock provider 验全链路 --

    /// 测试用 mock provider:返回预设的 CompletionResponse。
    struct MockProvider {
        response_text: String,
        fail: bool,
    }

    #[async_trait::async_trait]
    impl LlmProvider for MockProvider {
        async fn complete(
            &self,
            _request: CompletionRequest,
        ) -> anyhow::Result<crate::provider::CompletionResponse> {
            if self.fail {
                anyhow::bail!("mock provider 故意失败");
            }
            Ok(crate::provider::CompletionResponse {
                text: self.response_text.clone(),
                model: "mock".to_string(),
                usage: crate::provider::TokenUsage {
                    prompt_tokens: 0,
                    completion_tokens: 0,
                    total_tokens: 0,
                },
                tool_calls: None,
                reasoning_content: None,
            })
        }
        async fn stream(
            &self,
            _request: CompletionRequest,
        ) -> anyhow::Result<crate::provider::StreamResult> {
            anyhow::bail!("mock provider 不支持 stream")
        }
        fn name(&self) -> &str {
            "mock"
        }
    }

    #[tokio::test]
    async fn decompose_with_llm_success() {
        let coord = make_coord();
        let provider = MockProvider {
            // 合法 Plan JSON:read → write
            response_text: r#"{"steps":[
                {"id":"read","intent":"读取代码","tools":["read_file"],"deps":[]},
                {"id":"write","intent":"修改代码","tools":["write_file"],"deps":["read"]}
            ]}"#
                .to_string(),
            fail: false,
        };
        let result = coord
            .decompose_with_llm(
                &provider,
                "mock-model",
                "modify",
                "帮我读取并修改代码",
                &["read_file".into(), "write_file".into()],
            )
            .await
            .expect("合法 JSON + validate 通过应返回 Some");
        assert_eq!(result.subtasks.len(), 2);
        assert_eq!(result.subtasks[0].id, "read");
        assert_eq!(result.subtasks[1].id, "write");
        assert_eq!(result.subtasks[1].deps, vec!["read".to_string()]);
        assert!(!result.plan.is_empty());
    }

    #[tokio::test]
    async fn decompose_with_llm_provider_failure_returns_none() {
        // LLM 调用失败 → None(回退 ReAct,不 panic)
        let coord = make_coord();
        let provider = MockProvider {
            response_text: String::new(),
            fail: true,
        };
        let result = coord
            .decompose_with_llm(&provider, "m", "modify", "text", &[])
            .await;
        assert!(result.is_none(), "provider 失败应返 None 回退");
    }

    #[tokio::test]
    async fn decompose_with_llm_invalid_json_returns_none() {
        // 非 JSON → None
        let coord = make_coord();
        let provider = MockProvider {
            response_text: "这不是 JSON".to_string(),
            fail: false,
        };
        let result = coord
            .decompose_with_llm(&provider, "m", "modify", "text", &[])
            .await;
        assert!(result.is_none(), "非法 JSON 应返 None 回退");
    }

    #[tokio::test]
    async fn decompose_with_llm_empty_steps_returns_none() {
        // 空 steps 数组 → None
        let coord = make_coord();
        let provider = MockProvider {
            response_text: r#"{"steps":[]}"#.to_string(),
            fail: false,
        };
        let result = coord
            .decompose_with_llm(&provider, "m", "modify", "text", &[])
            .await;
        assert!(result.is_none(), "空 steps 应返 None 回退");
    }

    #[tokio::test]
    async fn decompose_with_llm_cycle_fails_validate_returns_none() {
        // LLM 出环依赖 → validate 拒 → None
        let coord = make_coord();
        let provider = MockProvider {
            response_text: r#"{"steps":[
                {"id":"a","intent":"x","tools":[],"deps":["b"]},
                {"id":"b","intent":"y","tools":[],"deps":["a"]}
            ]}"#
            .to_string(),
            fail: false,
        };
        let result = coord
            .decompose_with_llm(&provider, "m", "modify", "text", &[])
            .await;
        assert!(result.is_none(), "环依赖应 validate 拒返 None");
    }

    #[tokio::test]
    async fn decompose_with_llm_dangling_dep_fails_validate() {
        // 悬空 dep → validate 拒 → None
        let coord = make_coord();
        let provider = MockProvider {
            response_text: r#"{"steps":[
                {"id":"a","intent":"x","tools":[],"deps":["nonexistent"]}
            ]}"#
            .to_string(),
            fail: false,
        };
        let result = coord
            .decompose_with_llm(&provider, "m", "modify", "text", &[])
            .await;
        assert!(result.is_none(), "悬空 dep 应 validate 拒返 None");
    }

    #[tokio::test]
    async fn decompose_with_llm_single_step_no_tools_ok() {
        // 单步骤无工具(require_tools=false 允许)→ Ok
        let coord = make_coord();
        let provider = MockProvider {
            response_text: r#"{"steps":[{"id":"think","intent":"分析问题"}]}"#.to_string(),
            fail: false,
        };
        let result = coord
            .decompose_with_llm(&provider, "m", "chat", "解释一下", &[])
            .await
            .expect("单步无工具(require_tools=false)应通过");
        assert_eq!(result.subtasks.len(), 1);
    }

    #[tokio::test]
    async fn decompose_with_llm_markdown_fence_ok() {
        // LLM 包 ```json 围栏 → parse_plan_json 剥围栏后正常解析
        let coord = make_coord();
        let provider = MockProvider {
            response_text: "```json\n{\"steps\":[{\"id\":\"a\",\"intent\":\"x\"}]}\n```"
                .to_string(),
            fail: false,
        };
        let result = coord
            .decompose_with_llm(&provider, "m", "chat", "text", &[])
            .await;
        assert!(result.is_some(), "带 markdown 围栏的合法 JSON 应解析成功");
    }
}
