//! update_idea/update_project/update_task 部分更新回归测试
//!
//! 回归 P0 bug:LLM 客户端做部分更新(只传 description 不传 title)时,
//! 旧实现用 `arg_str_or(args, "title", "")` 取值,缺省 → 空串覆盖 existing
//! → title 被静默清空 → 数据丢失。
//!
//! 根本修:title/description(name)缺省回退 existing,而非空默认覆盖。
//! 本测试覆盖三个 handler 的「只传一个字段,另一字段保留 existing」语义。

use df_mcp::tools::{find, Ctx};
use df_storage::db::Database;
use serde_json::{json, Value};

/// 从 CallToolResult 取首个 text 块解析为 JSON。
fn result_json(res: &df_mcp::protocol::CallToolResult) -> Value {
    assert!(
        res.is_error != Some(true),
        "工具调用失败(is_error=true): {:?}",
        res.content
    );
    match res.content.first() {
        Some(df_mcp::protocol::ContentBlock::Text { text }) => {
            serde_json::from_str(text).expect("响应非合法 JSON")
        }
        other => panic!("预期 Text 块,实际: {other:?}"),
    }
}

/// 取嵌套对象 record(title/name/description 等业务字段在其下)。
fn record_of(v: &Value, key: &str) -> Value {
    v.get(key)
        .cloned()
        .unwrap_or_else(|| panic!("响应缺 `{key}` 字段: {v}"))
}

async fn setup() -> Ctx {
    let db = Database::open_in_memory().await.expect("open_in_memory");
    Ctx::new(std::sync::Arc::new(db))
}

async fn call(ctx: &Ctx, name: &str, args: Value) -> Value {
    let spec = find(name).expect("工具已注册");
    let res = (spec.handler)(ctx, args).await;
    result_json(&res)
}

// ============================================================
// update_idea:只传 description,title 必须保留 existing
// ============================================================

#[tokio::test]
async fn update_idea_keeps_title_when_only_description_sent() {
    let ctx = setup().await;
    // 先建一条想法:title="原始标题"
    let created = call(
        &ctx,
        "create_idea",
        json!({ "title": "原始标题", "description": "原始描述" }),
    )
    .await;
    let id = created["id"].as_str().expect("id").to_owned();

    // LLM 只传 description(不传 title)—— 旧实现会把 title 清空为 ""
    let updated = call(
        &ctx,
        "update_idea",
        json!({ "id": id, "description": "新描述" }),
    )
    .await;
    let idea = record_of(&updated, "idea");
    assert_eq!(idea["title"].as_str(), Some("原始标题"), "title 应保留 existing,不被空默认清空");
    assert_eq!(idea["description"].as_str(), Some("新描述"), "description 应更新为新值");
}

#[tokio::test]
async fn update_idea_keeps_description_when_only_title_sent() {
    let ctx = setup().await;
    let created = call(
        &ctx,
        "create_idea",
        json!({ "title": "原标题", "description": "原描述" }),
    )
    .await;
    let id = created["id"].as_str().expect("id").to_owned();

    let updated = call(&ctx, "update_idea", json!({ "id": id, "title": "新标题" })).await;
    let idea = record_of(&updated, "idea");
    assert_eq!(idea["title"].as_str(), Some("新标题"));
    assert_eq!(idea["description"].as_str(), Some("原描述"), "description 应保留 existing");
}

// ============================================================
// update_project:只传 description,name 必须保留 existing
// ============================================================

#[tokio::test]
async fn update_project_keeps_name_when_only_description_sent() {
    let ctx = setup().await;
    let created = call(
        &ctx,
        "create_project",
        json!({ "name": "原始项目", "description": "原始描述" }),
    )
    .await;
    let id = created["id"].as_str().expect("id").to_owned();

    let updated = call(
        &ctx,
        "update_project",
        json!({ "id": id, "description": "新描述" }),
    )
    .await;
    let project = record_of(&updated, "project");
    assert_eq!(project["name"].as_str(), Some("原始项目"), "name 应保留 existing");
    assert_eq!(project["description"].as_str(), Some("新描述"));
    // path/stack/idea_id 未传也应保留(existing 创建时为 None,这里间接保证不被改)
    assert_eq!(project["path"].as_str(), None);
}

#[tokio::test]
async fn update_project_keeps_status_when_not_sent() {
    // 状态字段缺省同样应保留 existing(旧实现默认 "planning" 会重置状态)
    let ctx = setup().await;
    let created = call(
        &ctx,
        "create_project",
        json!({ "name": "P", "description": "D", "status": "in_progress" }),
    )
    .await;
    let id = created["id"].as_str().expect("id").to_owned();

    let updated = call(
        &ctx,
        "update_project",
        json!({ "id": id, "description": "改描述" }),
    )
    .await;
    let project = record_of(&updated, "project");
    assert_eq!(
        project["status"].as_str(),
        Some("in_progress"),
        "status 应保留 existing,不被默认 planning 重置"
    );
}

// ============================================================
// update_task:只传 description,title/project_id 必须保留 existing
// ============================================================

#[tokio::test]
async fn update_task_keeps_title_and_project_when_only_description_sent() {
    let ctx = setup().await;
    let proj = call(
        &ctx,
        "create_project",
        json!({ "name": "所属项目", "description": "d" }),
    )
    .await;
    let project_id = proj["id"].as_str().expect("project id").to_owned();

    let created = call(
        &ctx,
        "create_task",
        json!({ "project_id": project_id, "title": "原始任务标题", "description": "原始描述" }),
    )
    .await;
    let id = created["id"].as_str().expect("id").to_owned();

    // 只传 description:旧实现 title 是必填会报错,description 缺省会清空(行为不一)
    // 根本修后三者都应保留 existing(或更新为新值)
    let updated = call(
        &ctx,
        "update_task",
        json!({ "id": id, "description": "新描述" }),
    )
    .await;
    let task = record_of(&updated, "task");
    assert_eq!(task["title"].as_str(), Some("原始任务标题"), "title 应保留 existing");
    assert_eq!(task["description"].as_str(), Some("新描述"));
    assert_eq!(task["project_id"].as_str(), Some(project_id.as_str()), "project_id 应保留 existing");
    // status 走状态机,update_task 不改也应保留
    assert_eq!(task["status"].as_str(), Some("todo"));
}

#[tokio::test]
async fn update_task_keeps_description_when_only_title_sent() {
    let ctx = setup().await;
    let proj = call(
        &ctx,
        "create_project",
        json!({ "name": "P2", "description": "d" }),
    )
    .await;
    let project_id = proj["id"].as_str().expect("project id").to_owned();
    let created = call(
        &ctx,
        "create_task",
        json!({ "project_id": project_id, "title": "原标题", "description": "原描述" }),
    )
    .await;
    let id = created["id"].as_str().expect("id").to_owned();

    let updated = call(&ctx, "update_task", json!({ "id": id, "title": "新标题" })).await;
    let task = record_of(&updated, "task");
    assert_eq!(task["title"].as_str(), Some("新标题"));
    assert_eq!(task["description"].as_str(), Some("原描述"), "description 应保留 existing");
}
