//! AI 节点 — 调用 LLM 完成文本生成/分析任务
//!
//! 工作流中无人值守的 AI 步骤：从节点 config 读取 OpenAI 兼容 provider 配置与
//! prompt，自建 client 调用一次 LLM，输出文本供下游节点消费。
//!
//! 与 AI Chat（侧边栏交互对话）的区别：AI Node 由 DAG Executor 自动驱动，
//! 适合嵌入自动化链路（如 想法 → AI分析 → 脚本落地 → 人工审批）。
//!
//! provider 解析/参数构造/自审 JSON 解析等纯逻辑见 `ai_node_helpers`(本文件只保留
//! AiNode / AiSelfReviewNode 的 struct + impl,impl 块约束)。

use std::sync::Arc;

use async_trait::async_trait;
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider};
use df_storage::crud::TaskRepo;
use df_storage::db::Database;
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};

// 抽离的纯函数/类型(glob 引入,tests `use super::*` 间接可见)
#[allow(unused_imports)]
use crate::ai_node_helpers::{
    gate_should_block, parse_review_json, provider_from_params, resolve_and_parse,
    truncate_for_summary, ResolvedProvider, REVIEW_SYSTEM_PROMPT,
};

/// AI 节点
///
/// 持有 Arc<Database> 在 NodeRegistry 注册时注入(state.rs build_registry 工厂闭包 move 捕获
/// db.clone(),对齐 TaskAdvanceNode 先例)。execute 从 NodeContext.config 读 provider 配置 +
/// 可选 task_id:有 task_id 时把产出落 task.output_json(AiNode 自审闭环,决策 a)。
pub struct AiNode {
    db: Arc<Database>,
}

impl AiNode {
    /// 构造节点(注册表工厂调用,注入数据库句柄)
    pub fn new(db: Arc<Database>) -> Self {
        Self { db }
    }
}

#[async_trait]
impl Node for AiNode {
    async fn execute(&self, ctx: NodeContext) -> NodeResult {
        tracing::info!("AiNode 执行: node_id={}", ctx.node_id);

        // FR-S1 注入链：provider 经 df_storage::secret 在 AiNode 内存解析，api_key 不进 config。
        let p = resolve_and_parse(&self.db, &ctx.config, &ctx.inputs).await?;
        let provider: Box<dyn LlmProvider> = provider_from_params(&p);

        // ── 构建消息 ──
        let mut messages = Vec::with_capacity(2);
        if let Some(sys) = p.system_prompt {
            messages.push(ChatMessage::system(sys));
        }
        messages.push(ChatMessage::user(p.prompt));

        let request = CompletionRequest {
            model: p.model.clone(),
            messages,
            temperature: p.temperature,
            max_tokens: p.max_tokens,
            stream: false,
            tools: None,
            tool_choice: None,
            reasoning_content: None,
        };

        tracing::info!(
            "AiNode 调用 LLM: model={}, base_url={}",
            p.provider.default_model,
            p.provider.base_url
        );
        let response = provider.complete(request).await?;

        tracing::info!(
            "AiNode 完成: model={}, prompt_tokens={}, completion_tokens={}",
            response.model,
            response.usage.prompt_tokens,
            response.usage.completion_tokens
        );

        let output = serde_json::json!({
            "text": response.text,
            "model": response.model,
            "usage": {
                "prompt_tokens": response.usage.prompt_tokens,
                "completion_tokens": response.usage.completion_tokens,
                "total_tokens": response.usage.total_tokens,
            },
        });

        // 决策 a(AiNode 自审闭环):有 task_id 时落产出到 task.output_json。
        // 走通用 update_field(output_json 已在 tasks 白名单,crud.rs:342-346),
        // 非 status 状态机收口字段,合法可写。落库失败不阻断节点返回(产出已在内存,工作流可继续)。
        if let Some(task_id) = ctx.config.get("task_id").and_then(|v| v.as_str()) {
            if let Ok(json_str) = serde_json::to_string(&output) {
                let repo = TaskRepo::new(&self.db);
                if let Err(e) = repo.update_field(task_id, "output_json", &json_str).await {
                    tracing::warn!(
                        task_id = task_id,
                        error = %e,
                        "AiNode 落 output_json 失败(产出已在内存,节点继续)"
                    );
                }
            }
        }

        Ok(NodeOutput::from_value(output))
    }

    fn schema(&self) -> NodeSchema {
        NodeSchema {
            params: serde_json::json!({
                "type": "object",
                "properties": {
                    "prompt": { "type": "string", "description": "用户提示词（若无则取上游 prompt 输出）" },
                    "system_prompt": { "type": "string", "description": "系统提示词（可选）" },
                    "provider_id": { "type": "string", "description": "AI Provider ID（FR-S1：密钥经 df_storage::secret 解析，不进 config；留空走默认 provider）" },
                    "model": { "type": "string", "description": "模型名（可选，留空用 record.default_model）" },
                    "temperature": { "type": "number", "description": "温度 0.0~2.0（可选）" },
                    "max_tokens": { "type": "integer", "description": "最大生成 token（可选，anthropic 协议无值时默认 4096）" },
                    "base_url": { "type": "string", "description": "（已废弃过渡）明文 API 地址，改用 provider_id" },
                    "api_key": { "type": "string", "description": "（已废弃过渡）明文 API 密钥，改用 provider_id；FR-S1 下经 secret 解析" }
                },
                // SW-260618-15: prompt/provider_id 均"留空走兜底"(prompt 取上游、provider_id 走默认 provider),与 required 矛盾。改 required=[] 对齐 execute 运行时,防前端按 schema 误拒合法配置。
                "required": []
            }),
            output: serde_json::json!({
                "type": "object",
                "properties": {
                    "text": { "type": "string" },
                    "model": { "type": "string" },
                    "usage": { "type": "object" }
                }
            }),
        }
    }

    fn node_type(&self) -> &str {
        "ai"
    }
}

// ============================================================
// ai_self_review 节点 — AI 自审闭环(决策 a 步骤③)
// ============================================================
//
// 复用 AiNode 的 provider 调用逻辑(parse_params + build_provider + complete),差异:
//   1. prompt 固定四维度审查模板(需求符合度/完整性/正确性/边界),不从 config/inputs 取 prompt
//   2. system_prompt 强约束「只输出 JSON」+ temperature=0
//   3. LLM 输出按结构化 JSON 解析,失败兜底 verdict=unknown(防 LLM 不按要求输出)
//   4. 自审输入:task.description(需求,经 ctx.config["task_id"] 读 task) + task.output_json.text(产出)
//   5. 自审结果写回 task.output_json 加 review 子字段(不覆盖 text,保留对照)
//   6. review 摘要塞 NodeOutput.data,供下游 human_review 经 DAG inputs["ai_self_review"] 读
//
// verdict 处理(首版保守):自审是辅助,最终决策权在人。verdict=fail 不主动 Err,
// 节点返 Ok 让 human_review 继续,人在审批卡看到 fail 结论再定(human reject 走 Err→工作流
// failed→②-4 回调退回 in_review)。

/// AI 自审节点(决策 a 步骤③)
pub struct AiSelfReviewNode {
    db: Arc<Database>,
}

impl AiSelfReviewNode {
    pub fn new(db: Arc<Database>) -> Self {
        Self { db }
    }

    /// 拼装自审 user prompt:任务需求 + 待审产出 + 四维度审查要求 + 输出格式。
    /// description / output_text 缺失时给占位(不报错,信任调用方注入合法 task_id)。
    fn build_review_prompt(description: &str, output_text: &str) -> String {
        format!(
            "\
## 任务需求
{description}

## 待审产出
{output_text}

## 审查维度
1. 需求符合度:产出是否覆盖需求描述的所有要点
2. 产出完整性:是否有遗漏、未完成的部分
3. 正确性:逻辑/事实/语法是否正确
4. 边界处理:异常输入、空值、错误路径是否考虑

## 输出格式(严格 JSON)
{{\"verdict\":\"pass\"|\"fail\",\"dimensions\":{{\"requirement_fit\":{{\"score\":0-10,\"issues\":[\"...\"]}},\"completeness\":{{\"score\":0-10,\"issues\":[\"...\"]}},\"correctness\":{{\"score\":0-10,\"issues\":[\"...\"]}},\"boundary\":{{\"score\":0-10,\"issues\":[\"...\"]}}}},\"summary\":\"一句话总结\",\"suggestions\":[\"改进建议\"]}}
verdict=fail 当且仅当任一维度 score<6 或有阻断性 issue。"
        )
    }
}

#[async_trait]
impl Node for AiSelfReviewNode {
    async fn execute(&self, ctx: NodeContext) -> NodeResult {
        tracing::info!("AiSelfReviewNode 执行: node_id={}", ctx.node_id);

        // FR-S1 注入链：provider 经 df_storage::secret 在 AiNode 内存解析，api_key 不进 config。
        let p = resolve_and_parse(&self.db, &ctx.config, &ctx.inputs).await?;

        // ── 读任务(需求 + 产出) ──
        // task_id 必填(自审对象是具体任务的产出);缺失报错而非静默跳过。
        let task_id = ctx
            .config
            .get("task_id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("AiSelfReviewNode 缺少必填参数: task_id"))?;

        let repo = TaskRepo::new(&self.db);
        let task = repo
            .get_by_id(task_id)
            .await
            .map_err(|e| anyhow::anyhow!("自审读任务失败: {}", e))?
            .ok_or_else(|| anyhow::anyhow!("自审任务 {} 不存在", task_id))?;

        // output_json 可能无(ai_execute 未跑或失败) → 待审产出占位提示,不报错。
        let output_text = task
            .output_json
            .as_deref()
            .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
            .and_then(|v| v.get("text").and_then(|t| t.as_str()).map(String::from))
            .unwrap_or_else(|| "(无产出,仅依据需求审查)".to_string());

        let prompt = Self::build_review_prompt(&task.description, &output_text);

        let provider: Box<dyn LlmProvider> = provider_from_params(&p);

        let request = CompletionRequest {
            model: p.model.clone(),
            messages: vec![
                ChatMessage::system(REVIEW_SYSTEM_PROMPT),
                ChatMessage::user(prompt),
            ],
            temperature: Some(0.0), // 强约束确定性输出
            max_tokens: p.max_tokens,
            stream: false,
            tools: None,
            tool_choice: None,
            reasoning_content: None,
        };

        tracing::info!(
            "AiSelfReviewNode 调用 LLM: model={}, base_url={}",
            p.provider.default_model,
            p.provider.base_url
        );
        let response = provider.complete(request).await?;

        // ── 解析自审 JSON(失败兜底 verdict=unknown) ──
        let review = parse_review_json(&response.text);
        let verdict = review
            .get("verdict")
            .and_then(|v| v.as_str())
            .unwrap_or("unknown")
            .to_string();
        // summary / suggestions 在 move review 前先取走(供 NodeOutput.data 平铺)
        let summary = review
            .get("summary")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        let suggestions = review
            .get("suggestions")
            .cloned()
            .unwrap_or(serde_json::json!([]));

        tracing::info!(
            "AiSelfReviewNode 完成: verdict={}, model={}",
            verdict,
            response.model
        );

        // ── 写回 task.output_json 加 review 子字段(不覆盖 text) ──
        // 读现有 output_json(若有),合并 review 子字段后整体写回。
        let mut output_obj: serde_json::Map<String, serde_json::Value> = task
            .output_json
            .as_deref()
            .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
            .and_then(|v| v.as_object().cloned())
            .unwrap_or_default();
        // review 子字段:自审结论 + reviewed_at + reviewer_model(便于前端展示与人对照)
        let mut review_with_meta = match review {
            serde_json::Value::Object(m) => m,
            other => {
                let mut m = serde_json::Map::new();
                m.insert("verdict".into(), other);
                m
            }
        };
        review_with_meta.insert(
            "reviewed_at".into(),
            serde_json::Value::Number(df_types::now_millis().into()),
        );
        review_with_meta.insert("reviewer_model".into(), serde_json::json!(response.model));
        output_obj.insert("review".into(), serde_json::Value::Object(review_with_meta));

        let merged = serde_json::Value::Object(output_obj);
        if let Ok(json_str) = serde_json::to_string(&merged) {
            if let Err(e) = repo.update_field(task_id, "output_json", &json_str).await {
                tracing::warn!(
                    task_id = task_id,
                    error = %e,
                    "AiSelfReviewNode 落 review 子字段失败(摘要已在 NodeOutput,节点继续)"
                );
            }
        }

        // ── NodeOutput.data 塞 review 摘要,供下游 human_review 经 inputs["ai_self_review"] 读 ──
        // human_review(HumanNode)从 config/inputs 拼 description;此处把 verdict/summary/suggestions
        // 平铺进 data,HumanNode 零改动即可透传(前端/模板层可据此拼审批卡描述)。
        let output = NodeOutput::from_value(serde_json::json!({
            "verdict": verdict,
            "summary": summary,
            "suggestions": suggestions,
            "review": merged.get("review").cloned().unwrap_or(serde_json::Value::Null),
            "model": response.model,
        }));

        // ── 阶段3: 自审闸门(F-260616-07 决策 a 步骤③) ──
        // config["gate"]==true 时,AiSelfReviewNode 从「自审辅助」升级为「DAG 节点闸门」:
        //   verdict="fail"   → 返回 Err → executor first_err 中止后续层(下游 human_review 不跑)
        //                       → 工作流 failed → ②-4 回调退回 in_review(对齐工作流失败语义)
        //   verdict="unknown" → 保持 Ok(LLM 输出不可靠时不阻断,人定权)
        //   verdict="pass"    → Ok 放行下游
        //
        // 落库在前(已 update_field output_json),故闸门 fail 时 task.output_json.review
        // 已含结论,前端/二次工作流可读自审详情(不丢失审查痕迹)。
        //
        // 设计选型(三方案对比,见 commit/设计文档):
        //   A) AiNode 内部门控(本方案):1 处 return Err,复用 executor first_err + ②-4 回调,
        //      零 executor 核心改动,零依赖暂缓的条件引擎(T-260614-11)。最简。
        //   B) DAG edges 条件 + ConditionEngine 求值:依赖 T-260614-11(暂缓),executor 当前
        //      完全不评估 edge.condition(topological_layers 无条件收录所有边),须先做条件
        //      引擎 Phase1+2 才能用 → 拆波。
        //   C) executor 闸门检查钩子(节点 execute 后 executor 读 output.verdict):改 DagExecutor
        //      核心循环,牵动所有节点,风险/范围不符「最简不破坏」。
        // 选 A:语义最贴近「自审结果作为闸门」(自审节点自行决定放行/阻断),且 gate 可按节点
        // config 开关(默认 false = 阶段2 行为不变,模板/前端零强制改动,向后兼容)。
        let gate_enabled = ctx
            .config
            .get("gate")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        if gate_should_block(gate_enabled, &verdict) {
            tracing::info!(
                node_id = %ctx.node_id,
                task_id,
                "AiSelfReviewNode 闸门触发: verdict=fail, 阻断下游(工作流将 failed)"
            );
            return Err(anyhow::anyhow!(
                "AI 自审闸门未通过(verdict=fail): {}",
                if summary.is_empty() { "自审结论 fail" } else { &summary }
            ));
        }

        Ok(output)
    }

    fn schema(&self) -> NodeSchema {
        NodeSchema {
            params: serde_json::json!({
                "type": "object",
                "properties": {
                    "task_id": { "type": "string", "description": "自审目标任务 ID(必填)" },
                    "provider_id": { "type": "string", "description": "AI Provider ID(FR-S1:密钥经 secret 解析不进 config;留空走默认 provider)" },
                    "model": { "type": "string", "description": "模型名(可选,留空用 record.default_model)" },
                    "max_tokens": { "type": "integer" },
                    "gate": { "type": "boolean", "description": "阶段3 闸门开关:false(默认)=自审辅助,verdict 仅透传展示;true=自审结果作 DAG 闸门,verdict=fail 返回 Err 阻断下游(工作流 failed → ②-4 退回),verdict=unknown/pass 放行" }
                },
                "required": ["task_id", "provider_id"]
            }),
            output: serde_json::json!({
                "type": "object",
                "properties": {
                    "verdict": { "type": "string", "enum": ["pass", "fail", "unknown"] },
                    "summary": { "type": "string" },
                    "suggestions": { "type": "array", "items": { "type": "string" } },
                    "review": { "type": "object" },
                    "model": { "type": "string" }
                }
            }),
        }
    }

    fn node_type(&self) -> &str {
        "ai_self_review"
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ai_node_helpers::{parse_params, resolve_provider};
    use df_storage::crud::{AiProviderRepo, ProjectRepo};
    use df_storage::db::Database;
    use df_storage::models::{AiProviderRecord, ProjectRecord, TaskRecord};
    use serde_json::json;
    use std::collections::HashMap;

    /// 测试用 ResolvedProvider 桩：固定 protocol/base_url/api_key/default_model。
    fn provider_stub() -> ResolvedProvider {
        ResolvedProvider {
            protocol: "openai_compat".to_string(),
            base_url: "https://api.example.com".to_string(),
            api_key: "sk-test".to_string(),
            default_model: "gpt-4o-mini".to_string(),
            model_pool: Vec::new(),
        }
    }

    /// 造带基础 prompt 的 config（provider 经 resolve_provider 解析，parse_params 只看 prompt），
    /// overrides 覆盖或追加。
    fn config_with(overrides: serde_json::Value) -> serde_json::Value {
        let mut base = json!({
            "prompt": "config-prompt"
        });
        if let (serde_json::Value::Object(b), serde_json::Value::Object(o)) = (&mut base, overrides) {
            for (k, v) in o {
                b.insert(k, v);
            }
        }
        base
    }

    fn empty_inputs() -> HashMap<String, NodeOutput> {
        HashMap::new()
    }

    #[test]
    fn missing_prompt_errors() {
        let config = json!({});
        let err = parse_params(&config, &empty_inputs(), provider_stub())
            .unwrap_err()
            .to_string();
        assert!(err.contains("prompt"), "缺 prompt 应报错, 实际: {}", err);
    }

    #[test]
    fn prompt_from_config_when_no_upstream() {
        let p =
            parse_params(&config_with(json!({})), &empty_inputs(), provider_stub()).unwrap();
        assert_eq!(p.prompt, "config-prompt");
    }

    #[test]
    fn prompt_prefers_upstream_input_over_config() {
        let mut inputs = empty_inputs();
        inputs.insert(
            "prompt".to_string(),
            NodeOutput::from_value(json!("upstream-prompt")),
        );
        let p =
            parse_params(&config_with(json!({})), &inputs, provider_stub()).unwrap();
        assert_eq!(p.prompt, "upstream-prompt", "上游输入应优先于 config.prompt");
    }

    #[test]
    fn optional_fields_parsed_when_present() {
        let p = parse_params(
            &config_with(json!({
                "model": "glm-4",
                "temperature": 0.3,
                "max_tokens": 1024,
                "system_prompt": "你是助手"
            })),
            &empty_inputs(),
            provider_stub(),
        )
        .unwrap();
        assert_eq!(p.model, "glm-4");
        assert_eq!(p.provider.protocol, "openai_compat");
        assert_eq!(p.temperature, Some(0.3));
        assert_eq!(p.max_tokens, Some(1024));
        assert_eq!(p.system_prompt.as_deref(), Some("你是助手"));
    }

    // ============================================================
    // resolve_provider 双路径测试(FR-S1 注入链核心)
    // ============================================================

    /// 内存 DB 插 provider(可选 is_default)，返回 (db, provider_id)。
    async fn setup_provider_db(
        provider_id: &str,
        is_default: bool,
        api_key: &str,
    ) -> (Database, String) {
        let db = Database::open_in_memory().await.expect("open_in_memory");
        let repo = AiProviderRepo::new(&db);
        repo.insert(AiProviderRecord {
            id: provider_id.to_string(),
            name: "测试 Provider".to_string(),
            provider_type: "openai_compat".to_string(),
            api_key: api_key.to_string(),
            base_url: "https://api.example.com".to_string(),
            default_model: "glm-4-flash".to_string(),
            models: None,
            model_configs: Vec::new(),
            is_default,
            config: None,
            created_at: "0".to_string(),
            updated_at: "0".to_string(),
            enabled: true,
            weight: 50,
        })
        .await
        .expect("insert provider");
        (db, provider_id.to_string())
    }

    /// 路径 1：provider_id 优先 → resolve_provider 走下沉解析(DB api_key 非空→直接返回)。
    #[tokio::test]
    async fn resolve_provider_by_id_prefers_db_key() {
        let (db, pid) = setup_provider_db("prov-1", true, "sk-from-db").await;
        let config = json!({ "provider_id": pid });
        let resolved = resolve_provider(&Arc::new(db), &config).await.unwrap();
        assert_eq!(resolved.api_key, "sk-from-db", "DB api_key 非空应直接返回");
        assert_eq!(resolved.base_url, "https://api.example.com");
        assert_eq!(resolved.protocol, "openai_compat");
        assert_eq!(
            resolved.default_model, "glm-4-flash",
            "无 config.model 时用 record.default_model"
        );
    }

    /// 路径 1：config.model 覆盖 record.default_model。
    #[tokio::test]
    async fn resolve_provider_config_model_overrides_record() {
        let (db, pid) = setup_provider_db("prov-1m", true, "sk-db").await;
        let config = json!({ "provider_id": pid, "model": "glm-4" });
        let resolved = resolve_provider(&Arc::new(db), &config).await.unwrap();
        assert_eq!(resolved.default_model, "glm-4");
    }

    /// 路径 1：provider_id 不存在 → 友好错误。
    #[tokio::test]
    async fn resolve_provider_unknown_id_errors() {
        let (db, _pid) = setup_provider_db("prov-2", true, "sk-db").await;
        let config = json!({ "provider_id": "non-existent" });
        let err = resolve_provider(&Arc::new(db), &config)
            .await
            .unwrap_err()
            .to_string();
        assert!(err.contains("不存在"), "未知 provider_id 应报错, 实际: {}", err);
    }

    /// 路径 1：provider_id 指向但密钥空(DB 空 + keyring 无)→ ensure_resolved_key 报错。
    #[tokio::test]
    async fn resolve_provider_empty_key_errors() {
        // api_key 空：DB 无 → keyring 读不到 → 空 → ensure 报错
        let (db, pid) = setup_provider_db("prov-empty", true, "").await;
        let config = json!({ "provider_id": pid });
        let err = resolve_provider(&Arc::new(db), &config)
            .await
            .unwrap_err()
            .to_string();
        assert!(
            err.contains("密钥") || err.contains("未读取到"),
            "空密钥应报错, 实际: {}",
            err
        );
    }

    /// 路径 2：老明文路径兼容(base_url+api_key 显式注入)→ 返回明文 + 不报错。
    #[tokio::test]
    async fn resolve_provider_legacy_plain_path_compat() {
        // 无 provider 且无 provider_id → 老明文路径兜底
        let db = Database::open_in_memory().await.expect("open_in_memory");
        let config = json!({
            "base_url": "https://legacy.example.com",
            "api_key": "sk-legacy"
        });
        let resolved = resolve_provider(&Arc::new(db), &config).await.unwrap();
        assert_eq!(resolved.api_key, "sk-legacy");
        assert_eq!(resolved.base_url, "https://legacy.example.com");
        assert_eq!(resolved.default_model, "gpt-4o-mini", "无 model 时占位兜底");
    }

    /// 路径 2：老明文路径空 key → ensure_resolved_key 报错。
    #[tokio::test]
    async fn resolve_provider_legacy_empty_key_errors() {
        let db = Database::open_in_memory().await.expect("open_in_memory");
        let config = json!({ "base_url": "https://x", "api_key": "" });
        let err = resolve_provider(&Arc::new(db), &config)
            .await
            .unwrap_err()
            .to_string();
        assert!(err.contains("密钥"), "老路径空 key 应报错, 实际: {}", err);
    }

    /// 路径 3：空兜底 → 无 provider_id 无明文 → 取 is_default=true 首条。
    #[tokio::test]
    async fn resolve_provider_fallback_default_provider() {
        let (db, _pid) = setup_provider_db("prov-default", true, "sk-default").await;
        let config = json!({}); // 无 provider_id 无明文
        let resolved = resolve_provider(&Arc::new(db), &config).await.unwrap();
        assert_eq!(resolved.api_key, "sk-default", "兜底应取 is_default 首条");
    }

    /// 路径 3：无任何 provider → 友好错误「未配置 AI Provider」。
    #[tokio::test]
    async fn resolve_provider_no_providers_errors() {
        let db = Database::open_in_memory().await.expect("open_in_memory");
        let config = json!({});
        let err = resolve_provider(&Arc::new(db), &config)
            .await
            .unwrap_err()
            .to_string();
        assert!(
            err.contains("未配置") || err.contains("Provider"),
            "无 provider 应友好报错, 实际: {}",
            err
        );
    }

    /// 路径 3：兜底无 is_default 时取首条(对齐 idea.rs build_default_provider)。
    #[tokio::test]
    async fn resolve_provider_fallback_first_when_no_default() {
        let (db, _pid) = setup_provider_db("prov-first", false, "sk-first").await;
        let config = json!({});
        let resolved = resolve_provider(&Arc::new(db), &config).await.unwrap();
        assert_eq!(resolved.api_key, "sk-first", "无 is_default 时取首条");
    }

    /// 真调 GLM 验证 provider 调用层（parse_params 已由上方单测覆盖，此处补 complete 端到端）
    ///
    /// `#[ignore]`：需真实 GLM 配置（env var），默认不跑。
    /// 跑法：`GLM_BASE_URL=... GLM_API_KEY=... GLM_MODEL=glm-4-flash \
    ///       cargo test -p df-nodes --lib glm_live_complete -- --ignored --nocapture`
    /// env var 缺失 → 跳过（非失败）。
    #[ignore = "需真实 GLM 配置(env var)"]
    #[tokio::test]
    async fn glm_live_complete() {
        let base_url = std::env::var("GLM_BASE_URL").ok().filter(|s| !s.is_empty());
        let api_key = std::env::var("GLM_API_KEY").ok().filter(|s| !s.is_empty());
        let (base_url, api_key) = match (base_url, api_key) {
            (Some(b), Some(k)) => (b, k),
            _ => {
                eprintln!("跳过: 未设 GLM_BASE_URL / GLM_API_KEY env var");
                return;
            }
        };
        let model = std::env::var("GLM_MODEL").unwrap_or_else(|_| "glm-4-flash".to_string());

        let provider: Box<dyn LlmProvider> =
            df_ai::build_provider("openai_compat", &base_url, &api_key, &model);
        let request = CompletionRequest {
            model: model.clone(),
            messages: vec![ChatMessage::user("只回复两个字:通过")],
            temperature: Some(0.0),
            max_tokens: Some(16),
            stream: false,
            tools: None,
            tool_choice: None,
            reasoning_content: None,
        };
        let response = provider.complete(request).await.expect("GLM 调用失败");
        assert!(!response.text.is_empty(), "GLM 返回空文本");
        println!(
            "GLM 响应: model={}, text={}, usage={:?}",
            response.model, response.text, response.usage
        );
    }

    // ============================================================
    // 决策 a 自审闭环单测 — output_json 写回 + review JSON 解析兜底
    // ============================================================

    /// 内存 DB 构造 helper:插父项目 + 指定 task,返回 (db, task_id)。
    async fn setup_task_db(task_output_json: Option<&str>) -> (Database, String) {
        let db = Database::open_in_memory().await.expect("open_in_memory");
        ProjectRepo::new(&db)
            .insert(ProjectRecord {
                id: "p1".to_string(),
                name: "proj".to_string(),
                description: "".to_string(),
                status: "planning".to_string(),
                idea_id: None,
                path: None,
                stack: None,
                created_at: "0".to_string(),
                updated_at: "0".to_string(),
            })
            .await
            .expect("insert project");
        TaskRepo::new(&db)
            .insert(TaskRecord {
                id: "t1".to_string(),
                project_id: "p1".to_string(),
                title: "t1".to_string(),
                description: "实现登录接口".to_string(),
                status: "testing".to_string(),
                priority: 2,
                branch_name: None,
                assignee: None,
                workflow_def_id: None,
                base_branch: None,
                review_rounds: 0,
                output_json: task_output_json.map(String::from),
                created_at: "0".to_string(),
                updated_at: "0".to_string(),
            })
            .await
            .expect("insert task");
        (db, "t1".to_string())
    }

    /// 决策 a 步骤②:AiNode.execute 落产出验证 — 有 task_id 时 output_json 写入。
    /// 用 mock provider 避免真调 LLM:自定义 Node 替换不便,改为直接验证 update_field 路径
    /// (execute 内部调 provider.complete 需真 LLM,此处降级验证 parse_review_json 兜底 +
    ///  直接 repo.update_field 路径,与 execute 落库逻辑同一条 crud.update_field)。
    #[tokio::test]
    async fn update_field_writes_output_json() {
        // 验证 output_json 在 tasks 白名单(update_field 不报「不允许的字段名」)。
        // 这正是 AiNode.execute / AiSelfReviewNode.execute 落产出的底层路径。
        let (db, task_id) = setup_task_db(None).await;
        let repo = TaskRepo::new(&db);
        let payload = r#"{"text":"产出内容","model":"glm-4","usage":{}}"#;
        let ok = repo
            .update_field(&task_id, "output_json", payload)
            .await
            .expect("update_field output_json");
        assert!(ok, "update_field 应返回 affected>0=true");
        let after = repo.get_by_id(&task_id).await.unwrap().unwrap();
        assert_eq!(
            after.output_json.as_deref(),
            Some(payload),
            "output_json 应被写入"
        );
        // 原始 text 字段(产出对照)保留
        let parsed: serde_json::Value = serde_json::from_str(after.output_json.as_deref().unwrap()).unwrap();
        assert_eq!(parsed["text"], json!("产出内容"));
    }

    /// 步骤③:parse_review_json 合规 JSON 直通(保留 verdict/dimensions/summary/suggestions)。
    #[test]
    fn parse_review_json_valid_passes_through() {
        let raw = r#"{"verdict":"pass","dimensions":{"requirement_fit":{"score":9,"issues":[]}},"summary":"符合需求","suggestions":["可加单测"]}"#;
        let v = parse_review_json(raw);
        assert_eq!(v["verdict"], json!("pass"));
        assert_eq!(v["summary"], json!("符合需求"));
        assert_eq!(v["suggestions"], json!(["可加单测"]));
        assert_eq!(v["dimensions"]["requirement_fit"]["score"], json!(9));
    }

    /// 步骤③:parse_review_json 非 JSON / 缺 verdict → 兜底 verdict=unknown + summary 含原文。
    #[test]
    fn parse_review_json_invalid_falls_back_unknown() {
        // 非 JSON
        let v = parse_review_json("这不是 JSON,LLM 没按要求输出");
        assert_eq!(v["verdict"], json!("unknown"));
        assert!(v["summary"].as_str().unwrap().contains("不是 JSON"));
        assert!(v["summary"].as_str().unwrap().contains("解析失败"));

        // JSON 但缺 verdict
        let v = parse_review_json(r#"{"foo":"bar"}"#);
        assert_eq!(v["verdict"], json!("unknown"));

        // JSON 非 object(数组)
        let v = parse_review_json("[1,2,3]");
        assert_eq!(v["verdict"], json!("unknown"));
    }

    /// 步骤③:LLM 偶尔包 ```json 代码块,parse_review_json 应剥离后解析成功。
    #[test]
    fn parse_review_json_strips_markdown_code_fence() {
        let raw = "```json\n{\"verdict\":\"fail\",\"summary\":\"缺边界处理\"}\n```";
        let v = parse_review_json(raw);
        assert_eq!(v["verdict"], json!("fail"));
        assert_eq!(v["summary"], json!("缺边界处理"));
    }

    /// 步骤③:truncate_for_summary 长文截断。
    #[test]
    fn truncate_for_summary_long_text() {
        let short = "短文";
        assert_eq!(truncate_for_summary(short), "短文");
        let long: String = "字".repeat(500);
        let t = truncate_for_summary(&long);
        assert!(t.ends_with("…"));
        assert!(t.chars().count() <= 302, "截断后含省略号应 ≈300 字");
    }

    /// 步骤③:AiSelfReviewNode.build_review_prompt 含需求 + 产出 + 四维度。
    #[test]
    fn build_review_prompt_contains_inputs() {
        let p = AiSelfReviewNode::build_review_prompt("登录接口", "已实现 /login");
        assert!(p.contains("登录接口"), "prompt 应含需求");
        assert!(p.contains("已实现 /login"), "prompt 应含产出");
        assert!(p.contains("需求符合度"), "prompt 应含四维度");
        assert!(p.contains("边界处理"));
        assert!(p.contains("verdict"), "prompt 应含输出格式约束");
    }

    // ============================================================
    // 阶段3 自审闸门(gate_should_block)单测
    // ============================================================
    //
    // gate 决策矩阵:
    //   gate=false(默认,阶段2 行为) → 任何 verdict 都放行(辅助模式)
    //   gate=true + verdict=pass     → 放行
    //   gate=true + verdict=unknown  → 放行(LLM 不可靠时不阻断,人定权)
    //   gate=true + verdict=fail     → 阻断(返回 Err,工作流 failed)

    #[test]
    fn gate_disabled_never_blocks_any_verdict() {
        // gate 默认 false(阶段2 兼容):无论 verdict 如何都不阻断,自审仅辅助展示
        assert!(!gate_should_block(false, "fail"), "gate 关闭时 fail 也不阻断");
        assert!(!gate_should_block(false, "pass"), "gate 关闭时 pass 放行");
        assert!(!gate_should_block(false, "unknown"), "gate 关闭时 unknown 放行");
    }

    #[test]
    fn gate_enabled_blocks_only_on_fail() {
        // gate 开启:仅 fail 阻断
        assert!(gate_should_block(true, "fail"), "gate 开 + fail 应阻断");
        assert!(!gate_should_block(true, "pass"), "gate 开 + pass 放行");
        assert!(
            !gate_should_block(true, "unknown"),
            "gate 开 + unknown 放行(LLM 不可靠时保人定权)"
        );
    }

    #[test]
    fn gate_unknown_verdict_does_not_block() {
        // 防御:verdict 非标准值(如解析异常)不应触发阻断(对齐 unknown 保守不阻断语义)
        assert!(
            !gate_should_block(true, "weird"),
            "非标准 verdict 不应阻断(对齐 unknown 保守)"
        );
        assert!(!gate_should_block(true, ""), "空 verdict 不应阻断");
    }
}
