//! 任务推进链 DAG 模板。
//!
//! 推进链前向转换的工作流拓扑描述(声明式,纯数据)。DagDef 只描述节点与边,
//! 执行逻辑靠 DagExecutor 驱动各 Node trait 的 execute —— 模板本身不跑逻辑。
//!
//! 三模板对应推进链三个前向转换点:
//!   - "in_progress": todo→in_progress,单 AiNode 执行任务
//!   - "testing":     in_review→testing,AiNode 自审 → HumanNode 核对(human reject 走 Err
//!                    触发工作流 failed → ②-4 回调退回 in_review)
//!   - "done":        testing→done,单 HumanNode 最终核对
//!
//! 不建 workflow_defs 表(模板少且稳定,硬编码;tasks.workflow_def_id 留 None)。
//!
//! config 设计:节点级 config 不硬编码 task_id / provider 等运行时参数 —— 全部留空。
//! 运行时 run_workflow(task_id, target_status) 调用方注入全局 config(task_id /
//! target_status / provider 配置),由 ④-1 deep_merge 与节点级 config 合并
//! (节点级覆盖全局)。模板只描述拓扑,不绑定具体任务。

use df_workflow::dag_def::DagDef;

/// 按目标状态返回推进链工作流模板。
///
/// `target_status` 取推进链前向目标状态(in_progress / testing / done)。
/// 未匹配返回 None —— 调用方降级(如直接 IPC 推进,不启工作流),不 panic。
///
/// 返回的 DagDef 节点级 config 为空对象或仅含拓扑级元信息(label 等),
/// 运行时参数由调用方全局 config 注入 + deep_merge。
pub fn template_for(target_status: &str) -> Option<DagDef> {
    match target_status {
        "in_progress" => Some(in_progress_template()),
        "testing" => Some(testing_template()),
        "done" => Some(done_template()),
        _ => None,
    }
}

/// todo → in_progress:单 AiNode 执行任务。
///
/// 拓扑:1 个 ai 节点。完成回调(②-3)推进 status 到 in_progress。
fn in_progress_template() -> DagDef {
    let mut dag = DagDef::new();
    // config 留空:prompt / provider / task_id 均由 run_workflow 全局 config 注入
    // (④-1 deep_merge 合并),模板只描述拓扑。
    dag.add_node(
        "ai_execute",
        "ai",
        serde_json::json!({}),
    );
    dag
}

/// in_review → testing:AiNode 自审 → HumanNode 核对。
///
/// 拓扑:ai → human 串行。ai_self_review 启用 gate:true:
///   - verdict=fail → AiSelfReviewNode 返回 Err → 工作流 failed(不经 human_review)
///     → ②-4 失败回调退回 in_review(review_rounds+=1)。
///   - verdict=unknown/pass → 放行 human_review,人定最终是否推进。
/// HumanNode reject 走 Err(②-5 已做)→ 工作流 failed → ②-4 失败回调退回 in_review。
/// 通过则完成回调(②-3)推进 status 到 testing。
fn testing_template() -> DagDef {
    let mut dag = DagDef::new();
    // 决策:ai_self_review 节点类型对齐 state.rs 注册的独立自审节点
    // (四维度 prompt + JSON 解析兜底 + 写回 output_json 加 review 子字段)。
    // gate=true 启用自审闸门(verdict=fail 阻断下游,工作流 failed)。
    dag.add_node(
        "ai_self_review",
        "ai_self_review",
        serde_json::json!({ "gate": true }),
    );
    dag.add_node(
        "human_review",
        "human",
        serde_json::json!({
            // 拓扑级元信息(非运行时参数):审批卡片默认文案。options 含「拒绝」
            // 触发 ②-5 reject → Err → 工作流 failed。
            "title": "核对 AI 自审结果",
            "options": ["同意", "拒绝"],
        }),
    );
    dag.add_edge("ai_self_review", "human_review");
    dag
}

/// testing → done:单 HumanNode 最终核对。
///
/// 拓扑:1 个 human 节点。通过则完成回调(②-3)推进 status 到 done。
/// reject 同理走 Err → failed → ②-4 退回。
fn done_template() -> DagDef {
    let mut dag = DagDef::new();
    dag.add_node(
        "human_final_review",
        "human",
        serde_json::json!({
            "title": "最终核对",
            "options": ["同意", "拒绝"],
        }),
    );
    dag
}

// ============================================================
// 单元测试 — 模板拓扑断言
// ============================================================

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

    #[test]
    fn unknown_status_returns_none() {
        assert!(template_for("unknown").is_none());
        assert!(template_for("").is_none());
        assert!(template_for("merged").is_none());
    }

    #[test]
    fn in_progress_template_single_ai_node() {
        let dag = template_for("in_progress").expect("in_progress 模板存在");
        assert_eq!(dag.nodes.len(), 1, "in_progress: 单节点");
        assert!(dag.edges.is_empty(), "in_progress: 无边");
        let node = dag.nodes.get("ai_execute").expect("ai_execute 节点存在");
        assert_eq!(node.node_type, "ai");
        // config 留空(运行时注入)
        assert!(
            node.config.as_object().map(|o| o.is_empty()).unwrap_or(true),
            "ai_execute config 应为空对象"
        );
    }

    #[test]
    fn testing_template_ai_to_human_serial() {
        let dag = template_for("testing").expect("testing 模板存在");
        assert_eq!(dag.nodes.len(), 2, "testing: ai + human 两节点");
        assert_eq!(dag.edges.len(), 1, "testing: 单串行边");
        // 节点类型(决策 a 步骤③:ai_self_review 独立节点类型)
        let ai = dag.nodes.get("ai_self_review").expect("ai_self_review 存在");
        assert_eq!(ai.node_type, "ai_self_review");
        // gate=true 启用自审闸门(verdict=fail 阻断下游)
        let gate = ai
            .config
            .get("gate")
            .and_then(|v| v.as_bool())
            .expect("ai_self_review config 应含 gate");
        assert!(gate, "testing 模板 ai_self_review 应启用 gate:true(闸门)");
        let human = dag.nodes.get("human_review").expect("human_review 存在");
        assert_eq!(human.node_type, "human");
        // 边方向:ai → human
        let edge = &dag.edges[0];
        assert_eq!(edge.source, "ai_self_review");
        assert_eq!(edge.target, "human_review");
        // human options 含「拒绝」(触发 ②-5 reject)
        let options = human
            .config
            .get("options")
            .and_then(|v| v.as_array())
            .expect("human_review options 存在");
        let opts: Vec<&str> = options.iter().filter_map(|v| v.as_str()).collect();
        assert!(opts.contains(&"拒绝"), "options 应含「拒绝」触发 reject");
    }

    #[test]
    fn done_template_single_human_node() {
        let dag = template_for("done").expect("done 模板存在");
        assert_eq!(dag.nodes.len(), 1, "done: 单节点");
        assert!(dag.edges.is_empty(), "done: 无边");
        let node = dag.nodes.get("human_final_review").expect("human_final_review 存在");
        assert_eq!(node.node_type, "human");
    }

    // ---------- ②-3 / ②-4 回调语义对齐(CR-06 观察项 ② 测试补强) ----------
    //
    // 工作流完成(②-3)/失败(②-4)回调在 src-tauri/src/commands/workflow.rs spawn 闭包里,
    // 回调的核心计算是:template_for 选模板 + regression_target(target) 推退回态 +
    // advance_task_atomic 落库。这里锁定 template_for 三模板对回调语义的约束,
    // 保证回调侧(workflow.rs)的 target_status 取值与模板存在性一致 ——
    // 即回调只会对 template_for 返回 Some 的三个 target 触发,其余 target 模板不存在、
    // 工作流压根不会启动,回调自然不触发。

    /// 三模板对应的 target_status 集合(workflow.rs ②-3/②-4 回调仅对这些 target 触发)
    const CALLBACK_TARGETS: &[&str] = &["in_progress", "testing", "done"];

    #[test]
    fn callback_targets_all_have_templates() {
        // ②-3/②-4 回调依赖 template_for(target) 非空 —— 凡是能进回调的 target 必有模板。
        // 若新增一个回调 target 但忘了加模板,这里会先红。
        for target in CALLBACK_TARGETS {
            assert!(
                template_for(target).is_some(),
                "回调 target {target:?} 必须有对应模板(template_for 非 None)"
            );
        }
    }

    #[test]
    fn non_callback_targets_have_no_template() {
        // 反向:非推进链 target(todo/in_review/blocked/cancelled/未知) 无模板,
        // 工作流不启动,②-3/②-4 回调不触发 —— 锁定回调边界。
        for non_target in ["todo", "in_review", "blocked", "cancelled", "merged", ""] {
            assert!(
                template_for(non_target).is_none(),
                "非推进链 target {non_target:?} 不应有模板(否则回调边界被扩大)"
            );
        }
    }

    #[test]
    fn testing_and_done_templates_have_reject_path() {
        // ②-4 失败回调依赖模板内 human reject 走 Err → 工作流 failed → regression_target。
        // testing(done 同理)模板的 human 节点 options 必须含「拒绝」,否则失败回调不可达。
        // (in_progress 模板只有 ai 节点,失败是 ai 异常,无 reject 路径 —— 单独验证。)
        for target in ["testing", "done"] {
            let dag = template_for(target).unwrap_or_else(|| panic!("{target} 模板应存在"));
            let human = dag.nodes.values().find(|n| n.node_type == "human").unwrap_or_else(|| {
                panic!("{target} 模板应含 human 节点(承载 reject → failed → ②-4 回调)")
            });
            let options = human
                .config
                .get("options")
                .and_then(|v| v.as_array())
                .unwrap_or_else(|| panic!("{target} human 节点 config 应有 options 数组"));
            let opts: Vec<&str> = options.iter().filter_map(|v| v.as_str()).collect();
            assert!(
                opts.contains(&"拒绝"),
                "{target} human options 应含「拒绝」(否则 ②-4 失败回调路径不可达)"
            );
        }
    }
}
