//! ProjectRepo 软删回收站 + 级联清理 的集成测试。
//!
//! 覆盖三类行为:
//! 1. purge_with_descendants — 事务级联删 branches→releases→tasks→projects。
//! 2. soft_delete / list_active / list_deleted / restore — 回收站生命周期。
//! 3. allowed_columns_for 按表隔离 — update_field 误传跨表列名在白名单阶段被拒。

use df_storage::crud::{BranchRepo, ProjectRepo, ReleaseRepo, TaskRepo};
use df_storage::db::Database;
use df_storage::models::{BranchRecord, ProjectRecord, ReleaseRecord, TaskRecord};
use df_types::types::{ProjectStatus, TaskStatus};

// ---------- fixtures ----------

fn now_ts() -> String {
    "1700000000000".to_string()
}

fn project(id: &str) -> ProjectRecord {
    ProjectRecord {
        id: id.to_string(),
        name: format!("proj-{id}"),
        description: "desc".to_string(),
        status: ProjectStatus::Planning,
        idea_id: None,
        path: None,
        stack: None,
        created_at: now_ts(),
        updated_at: now_ts(),
    }
}

fn task(id: &str, project_id: &str) -> TaskRecord {
    TaskRecord {
        id: id.to_string(),
        project_id: project_id.to_string(),
        title: format!("task-{id}"),
        description: "desc".to_string(),
        status: TaskStatus::Todo,
        priority: 1,
        branch_name: None,
        assignee: None,
        workflow_def_id: None,
        base_branch: None,
        review_rounds: 0,
        output_json: None,
        idea_id: None,
        queue: "todo".to_string(),
        parent_id: None,
        content_json: None,
        created_at: now_ts(),
        updated_at: now_ts(),
    }
}

fn release(id: &str, project_id: &str) -> ReleaseRecord {
    ReleaseRecord {
        id: id.to_string(),
        project_id: project_id.to_string(),
        version: "0.1.0".to_string(),
        status: "draft".to_string(),
        task_ids: "[]".to_string(),
        changelog: None,
        created_at: now_ts(),
        released_at: None,
    }
}

fn branch(id: &str, project_id: &str) -> BranchRecord {
    BranchRecord {
        id: id.to_string(),
        project_id: project_id.to_string(),
        task_id: None,
        name: format!("br-{id}"),
        base: "main".to_string(),
        status: "open".to_string(),
        created_at: now_ts(),
        updated_at: now_ts(),
        merged_at: None,
    }
}

async fn setup() -> (ProjectRepo, TaskRepo, ReleaseRepo, BranchRepo) {
    let db = Database::open_in_memory().await.expect("open_in_memory");
    (
        ProjectRepo::new(&db),
        TaskRepo::new(&db),
        ReleaseRepo::new(&db),
        BranchRepo::new(&db),
    )
}

// ============================================================
// 1. purge_with_descendants — 级联删除
// ============================================================

#[tokio::test]
async fn purge_with_descendants_removes_project_and_all_children() {
    let (projects, tasks, releases, branches) = setup().await;

    projects.insert(project("p1")).await.unwrap();
    tasks.insert(task("t1", "p1")).await.unwrap();
    tasks.insert(task("t2", "p1")).await.unwrap();
    releases.insert(release("r1", "p1")).await.unwrap();
    branches.insert(branch("b1", "p1")).await.unwrap();
    branches.insert(branch("b2", "p1")).await.unwrap();

    // 另一个项目的子记录不应被波及(只删 p1 的)
    projects.insert(project("p2")).await.unwrap();
    tasks.insert(task("t3", "p2")).await.unwrap();

    let affected = projects.purge_with_descendants("p1").await.unwrap();
    assert!(affected, "应命中 p1");

    // 四表对应记录全消失
    assert!(projects.get_by_id("p1").await.unwrap().is_none());
    assert!(tasks.get_by_id("t1").await.unwrap().is_none());
    assert!(tasks.get_by_id("t2").await.unwrap().is_none());
    assert!(releases.get_by_id("r1").await.unwrap().is_none());
    assert!(branches.get_by_id("b1").await.unwrap().is_none());
    assert!(branches.get_by_id("b2").await.unwrap().is_none());

    // 邻居项目 p2 完好
    assert!(projects.get_by_id("p2").await.unwrap().is_some());
    assert!(tasks.get_by_id("t3").await.unwrap().is_some());
}

#[tokio::test]
async fn purge_with_descendants_unknown_id_returns_false() {
    let (projects, _t, _r, _b) = setup().await;
    let affected = projects.purge_with_descendants("ghost").await.unwrap();
    assert!(!affected, "不存在的 id 不命中");
}

#[tokio::test]
async fn purge_with_descendants_is_atomic_on_missing_child_table_row() {
    // 事务性验证:即使 project 存在但被删语句之间无冲突,purge 应整体成功或整体不变。
    // 这里验证 commit 成功路径下子表清空、父表删除;反向证明未部分提交(若部分提交,
    // projects 会在事务中途被删但 branches 残留 —— 通过重新 purge 验证幂等空操作)。
    let (projects, tasks, _releases, branches) = setup().await;
    projects.insert(project("p1")).await.unwrap();
    tasks.insert(task("t1", "p1")).await.unwrap();
    branches.insert(branch("b1", "p1")).await.unwrap();

    assert!(projects.purge_with_descendants("p1").await.unwrap());

    // 事务已提交:父表与全部子表一致清空(不会出现父删子留)
    assert!(projects.get_by_id("p1").await.unwrap().is_none());
    assert!(tasks.get_by_id("t1").await.unwrap().is_none());
    assert!(branches.get_by_id("b1").await.unwrap().is_none());

    // 再次 purge 已不存在的 id → false,且无副作用
    assert!(!projects.purge_with_descendants("p1").await.unwrap());
}

// ============================================================
// 2. soft_delete / list_active / list_deleted / restore
// ============================================================

#[tokio::test]
async fn soft_delete_moves_project_out_of_active_into_deleted() {
    let (projects, _t, _r, _b) = setup().await;
    projects.insert(project("p1")).await.unwrap();
    projects.insert(project("p2")).await.unwrap();

    let ok = projects.soft_delete("p1").await.unwrap();
    assert!(ok, "软删 p1 应命中");

    let active = projects.list_active().await.unwrap();
    let active_ids: Vec<_> = active.iter().map(|p| p.id.as_str()).collect();
    assert!(!active_ids.contains(&"p1"), "p1 不应在 active");
    assert!(active_ids.contains(&"p2"), "p2 仍在 active");

    let deleted = projects.list_deleted().await.unwrap();
    let deleted_ids: Vec<_> = deleted.iter().map(|p| p.id.as_str()).collect();
    assert!(deleted_ids.contains(&"p1"), "p1 应在 deleted");
    assert!(!deleted_ids.contains(&"p2"), "p2 不在 deleted");
}

#[tokio::test]
async fn restore_moves_project_back_into_active() {
    let (projects, _t, _r, _b) = setup().await;
    projects.insert(project("p1")).await.unwrap();
    projects.soft_delete("p1").await.unwrap();

    let ok = projects.restore("p1").await.unwrap();
    assert!(ok, "恢复 p1 应命中");

    let active_ids: Vec<_> = projects
        .list_active()
        .await
        .unwrap()
        .into_iter()
        .map(|p| p.id)
        .collect();
    assert!(active_ids.contains(&"p1".to_string()), "p1 恢复后回到 active");

    let deleted_ids: Vec<_> = projects
        .list_deleted()
        .await
        .unwrap()
        .into_iter()
        .map(|p| p.id)
        .collect();
    assert!(!deleted_ids.contains(&"p1".to_string()), "p1 恢复后离开 deleted");
}

#[tokio::test]
async fn soft_delete_on_already_deleted_returns_false() {
    // WHERE deleted_at IS NULL 守卫:已删项二次软删不命中
    let (projects, _t, _r, _b) = setup().await;
    projects.insert(project("p1")).await.unwrap();

    assert!(projects.soft_delete("p1").await.unwrap());
    // 第二次对已删项 → false(守卫生效)
    assert!(
        !projects.soft_delete("p1").await.unwrap(),
        "已删项二次软删应返回 false"
    );
}

#[tokio::test]
async fn soft_delete_unknown_id_returns_false() {
    let (projects, _t, _r, _b) = setup().await;
    assert!(!projects.soft_delete("ghost").await.unwrap());
}

#[tokio::test]
async fn restore_unknown_id_returns_false() {
    let (projects, _t, _r, _b) = setup().await;
    assert!(!projects.restore("ghost").await.unwrap());
}

// ============================================================
// 3. allowed_columns_for — 按表隔离
// ============================================================

#[tokio::test]
async fn update_field_rejects_cross_table_column_tasks_name() {
    // tasks 表白名单无 "name"(那是 projects/branches 的列)→ Err
    let (projects, tasks, _r, _b) = setup().await;
    projects.insert(project("p1")).await.unwrap();
    tasks.insert(task("t1", "p1")).await.unwrap();

    let res = tasks.update_field("t1", "name", "x").await;
    assert!(res.is_err(), "tasks 表无 name 列,应被白名单拒绝");

    // 对照:tasks 的合法列 title 通过
    let ok = tasks.update_field("t1", "title", "renamed").await.unwrap();
    assert!(ok);
    let rec = tasks.get_by_id("t1").await.unwrap().unwrap();
    assert_eq!(rec.title, "renamed");
}

#[tokio::test]
async fn update_field_rejects_tasks_status() {
    // D-260616-04 status 收口:status 已从 tasks 白名单移除,所有 status 改动须走
    // advance_status_atomic(CAS SQL 独立路径,不经通用 update_field)。update_field
    // 改 status 应在白名单阶段被拒(防 update_task IPC / AI 工具旁路绕过状态机与 review_rounds 累加)。
    let (projects, tasks, _r, _b) = setup().await;
    projects.insert(project("p1")).await.unwrap();
    tasks.insert(task("t1", "p1")).await.unwrap();

    let res = tasks.update_field("t1", "status", "done").await;
    assert!(
        res.is_err(),
        "status 不在 tasks 白名单,应被拒(改走 advance_status_atomic)"
    );

    // 对照:status 未被改写,仍为初始值(task fixture 的初始 status)
    let rec = tasks.get_by_id("t1").await.unwrap().unwrap();
    assert_ne!(rec.status.as_str(), "done", "白名单拒绝后 status 不应被改写");
}

#[tokio::test]
async fn update_field_rejects_projects_deleted_at() {
    // deleted_at 是专用路径列(soft_delete/restore),不进通用白名单 → 拒绝
    let (projects, _t, _r, _b) = setup().await;
    projects.insert(project("p1")).await.unwrap();

    let res = projects.update_field("p1", "deleted_at", "123").await;
    assert!(
        res.is_err(),
        "deleted_at 不在 projects 白名单,应被拒(专用路径才能写)"
    );
}
