//! Conventional Commit 解析与执行质量指标（飞轮 L0 数据地基）。
//!
//! 从 git commit message 解析出 type/scope/返工/CI 自修信号，写入 commit_metrics 表，
//! 取代枯竭的「复盘笔记解析」路径。纯解析逻辑（`parse_conventional`）与 DB 无关，可独立单测。

use rusqlite::Connection;
use serde::{Deserialize, Serialize};

/// 一条 commit 的解析结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCommit {
    /// conventional type（feat/fix/chore...）；None 表示非 conventional commit。
    pub commit_type: Option<String>,
    /// scope（如 estate1/cad）；None 表示无 scope。
    pub scope: Option<String>,
    /// 是否返工：type == fix。
    pub is_rework: bool,
    /// 是否 CI 自动修复（含 [DeepSeek-V3]/[R1] 等标记），聚合返工率时剔除。
    pub is_ci_auto: bool,
}

/// CI 自动修复 commit 的标记（来自 enterprise-system CI：`fix(ci): auto-fix ... [DeepSeek-V3]`）。
const CI_AUTO_MARKERS: [&str; 3] = ["[DeepSeek", "[R1]", "[V3]"];

/// 解析一条 commit message 为 Conventional Commit 结构。
///
/// 纯函数，不触碰 DB。无法识别为 conventional 的（merge commit、随手写的）
/// `commit_type` 记为 None，聚合时单列、不污染 fix 率。
pub fn parse_conventional(message: &str) -> ParsedCommit {
    let is_ci_auto = CI_AUTO_MARKERS.iter().any(|m| message.contains(m));

    // 只看首行（subject）
    let subject = message.lines().next().unwrap_or("").trim();

    // 必须有 "<prefix>: <desc>" 结构
    let prefix = match subject.split_once(':') {
        Some((p, _desc)) => p.trim(),
        None => return non_conventional(is_ci_auto),
    };

    // 去掉 breaking change 的末尾 '!'
    let prefix = prefix.trim_end_matches('!');

    // 拆出 type 与可选 scope：type(scope)
    let (typ, scope) = match prefix.split_once('(') {
        Some((t, rest)) => {
            let scope = rest
                .strip_suffix(')')
                .map(|s| s.trim().to_string())
                .filter(|s| !s.is_empty());
            (t.trim(), scope)
        }
        None => (prefix, None),
    };

    // type 必须是纯小写字母（feat/fix/...），否则视为非 conventional（如 "Merge pull request"）
    if typ.is_empty() || !typ.chars().all(|c| c.is_ascii_lowercase()) {
        return non_conventional(is_ci_auto);
    }

    ParsedCommit {
        is_rework: typ == "fix",
        commit_type: Some(typ.to_string()),
        scope,
        is_ci_auto,
    }
}

fn non_conventional(is_ci_auto: bool) -> ParsedCommit {
    ParsedCommit {
        commit_type: None,
        scope: None,
        is_rework: false,
        is_ci_auto,
    }
}

/// 解析 commit message 中的 `Rules-Applied: R01,R06` trailer（飞轮 R1 规则有效性数据源）。
/// 只接受 R+数字 形式的 token，规范化为大写去重逗号串；无 trailer 或无有效 token 返回 None。
pub fn parse_rules_applied(message: &str) -> Option<String> {
    for line in message.lines() {
        let trimmed = line.trim();
        let Some(rest) = trimmed
            .strip_prefix("Rules-Applied:")
            .or_else(|| trimmed.strip_prefix("rules-applied:"))
            .or_else(|| trimmed.strip_prefix("Rules-applied:"))
        else {
            continue;
        };
        let rest = rest.trim();
        let mut ids: Vec<String> = rest
            .split([',', ' ', '，'])
            .map(|t| t.trim().to_uppercase())
            .filter(|t| {
                t.len() >= 2
                    && t.starts_with('R')
                    && t[1..].chars().all(|c| c.is_ascii_digit())
            })
            .collect();
        ids.sort();
        ids.dedup();
        return if ids.is_empty() { None } else { Some(ids.join(",")) };
    }
    None
}

/// 解析并写入一条 commit 记录（sha 为主键，重复写入忽略）。
pub fn record_commit(
    conn: &Connection,
    project_id: &str,
    sha: &str,
    message: &str,
    committed_at: Option<&str>,
) -> Result<(), String> {
    let p = parse_conventional(message);
    let rules_applied = parse_rules_applied(message);
    conn.execute(
        "INSERT OR IGNORE INTO commit_metrics
            (sha, project_id, commit_type, scope, is_rework, is_ci_auto, committed_at, rules_applied)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
        rusqlite::params![
            sha,
            project_id,
            p.commit_type,
            p.scope,
            p.is_rework as i32,
            p.is_ci_auto as i32,
            committed_at,
            rules_applied,
        ],
    )
    .map_err(|e| e.to_string())?;
    Ok(())
}

/// 单条规则的命中统计（跨项目聚合）
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RuleHitStat {
    pub rule_id: String,
    /// 附带该规则的 commit 总数
    pub hits: u32,
    /// 其中 type=fix（非 CI 自修）的返工 commit 数——规则触发了但仍出问题的信号
    pub rework_hits: u32,
    /// 覆盖的项目数
    pub project_count: u32,
}

/// 聚合所有项目的 Rules-Applied 命中 × 返工交叉统计（供梦核置信度分析）。
pub fn rule_hit_stats(conn: &Connection) -> Result<Vec<RuleHitStat>, String> {
    let mut stmt = conn
        .prepare(
            "SELECT rules_applied, is_rework, is_ci_auto, project_id
             FROM commit_metrics WHERE rules_applied IS NOT NULL",
        )
        .map_err(|e| e.to_string())?;
    let rows = stmt
        .query_map([], |r| {
            Ok((
                r.get::<_, String>(0)?,
                r.get::<_, i64>(1)? != 0,
                r.get::<_, i64>(2)? != 0,
                r.get::<_, String>(3)?,
            ))
        })
        .map_err(|e| e.to_string())?;

    use std::collections::{BTreeMap, HashSet};
    let mut hits: BTreeMap<String, (u32, u32, HashSet<String>)> = BTreeMap::new();
    for row in rows {
        let (rules, is_rework, is_ci_auto, project_id) = row.map_err(|e| e.to_string())?;
        for rule in rules.split(',').map(str::trim).filter(|s| !s.is_empty()) {
            let entry = hits.entry(rule.to_string()).or_default();
            entry.0 += 1;
            if is_rework && !is_ci_auto {
                entry.1 += 1;
            }
            entry.2.insert(project_id.clone());
        }
    }

    Ok(hits
        .into_iter()
        .map(|(rule_id, (h, rw, projects))| RuleHitStat {
            rule_id,
            hits: h,
            rework_hits: rw,
            project_count: projects.len() as u32,
        })
        .collect())
}

/// 项目的有效返工次数：type=fix 且非 CI 自动修复。
pub fn rework_count(conn: &Connection, project_id: &str) -> Result<u32, String> {
    conn.query_row(
        "SELECT COUNT(*) FROM commit_metrics
         WHERE project_id = ?1 AND is_rework = 1 AND is_ci_auto = 0",
        rusqlite::params![project_id],
        |r| r.get::<_, i64>(0),
    )
    .map(|n| n as u32)
    .map_err(|e| e.to_string())
}

/// commit 规范化统计：(总数, conventional 数)。供数据可信度提示（卡 D）使用。
pub fn conventional_stats(conn: &Connection, project_id: &str) -> Result<(u32, u32), String> {
    let total: i64 = conn
        .query_row(
            "SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1",
            rusqlite::params![project_id],
            |r| r.get(0),
        )
        .map_err(|e| e.to_string())?;
    let conv: i64 = conn
        .query_row(
            "SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1 AND commit_type IS NOT NULL",
            rusqlite::params![project_id],
            |r| r.get(0),
        )
        .map_err(|e| e.to_string())?;
    Ok((total as u32, conv as u32))
}

/// 项目 commit 执行质量统计（供前端数据可信度展示）。
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CommitStats {
    pub total: u32,
    pub conventional: u32,
    pub rework: u32,
}

/// 查询某项目的 commit 执行质量统计（总数 / conventional 数 / 有效返工数）。
#[tauri::command]
pub fn get_commit_stats(project_id: String) -> Result<CommitStats, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let (total, conventional) = conventional_stats(&conn, &project_id)?;
    let rework = rework_count(&conn, &project_id)?;
    Ok(CommitStats {
        total,
        conventional,
        rework,
    })
}

// ── 全量历史导入 ──────────────────────────────────────────────────────────────

const FULL_HISTORY_LIMIT: usize = 500;

/// 全量历史补录结果。
#[derive(Debug, Serialize)]
pub struct IngestResult {
    /// 本次新写入 commit 数
    pub imported: u32,
    /// 已存在（INSERT OR IGNORE 跳过）
    pub skipped: u32,
}

/// 读取 git log 最近 limit 条：返回 (sha, subject, committed_at_iso)。
fn read_git_log(root: &std::path::Path, limit: usize) -> Result<Vec<(String, String, String)>, String> {
    const FMT: &str = "--format=%H%x1f%s%x1f%cI";
    let out = crate::silent_cmd("git")
        .args(["log", "-n", &limit.to_string(), FMT])
        .current_dir(root)
        .output()
        .map_err(|e| format!("git log 失败: {e}"))?;
    if !out.status.success() {
        return Err(format!("git log 错误: {}", String::from_utf8_lossy(&out.stderr)));
    }
    Ok(String::from_utf8_lossy(&out.stdout)
        .lines()
        .filter_map(|line| {
            let mut parts = line.split('\u{1f}');
            let sha = parts.next()?.trim().to_string();
            if sha.is_empty() { return None; }
            let subject = parts.next().unwrap_or("").to_string();
            let date = parts.next().unwrap_or("").to_string();
            Some((sha, subject, date))
        })
        .collect())
}

/// 全量历史导入：扫描最近 500 条 commit，幂等写入 commit_metrics 表。
/// 与 buff 的增量写入互不干扰（同 sha INSERT OR IGNORE）。
/// 若该路径在 blueprint_buffs 中已有 UUID project_id，优先用 UUID，保持与增量写入一致。
#[tauri::command]
pub fn ingest_full_git_history(
    project_id: String,
    project_path: String,
) -> Result<IngestResult, String> {
    let root = std::path::Path::new(project_path.trim_end_matches(['/', '\\']));
    if !root.exists() {
        return Err(format!("目录不存在: {}", root.display()));
    }
    let commits = read_git_log(root, FULL_HISTORY_LIMIT)?;
    let total = commits.len() as u32;

    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;

    // 优先用 blueprint_buffs 里的 UUID project_id，避免同一 sha 用两个不同 project_id 冲突
    let effective_id = conn
        .query_row(
            "SELECT project_id FROM blueprint_buffs WHERE project_path = ?1",
            rusqlite::params![project_path],
            |r| r.get::<_, String>(0),
        )
        .unwrap_or(project_id);

    let tx = conn.unchecked_transaction().map_err(|e| e.to_string())?;
    let mut imported = 0u32;
    for (sha, subject, date) in &commits {
        let p = parse_conventional(subject);
        let n = conn.execute(
            "INSERT OR IGNORE INTO commit_metrics
                (sha, project_id, commit_type, scope, is_rework, is_ci_auto, committed_at)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
            rusqlite::params![
                sha, effective_id, p.commit_type, p.scope,
                p.is_rework as i32, p.is_ci_auto as i32,
                if date.is_empty() { None } else { Some(date.as_str()) },
            ],
        ).map_err(|e| e.to_string())?;
        imported += n as u32;
    }
    tx.commit().map_err(|e| e.to_string())?;
    Ok(IngestResult { imported, skipped: total - imported })
}

// ── 跨项目 Git 健康度聚合 ─────────────────────────────────────────────────────

/// 单个 scope 的活跃度摘要。
#[derive(Debug, Serialize)]
pub struct ScopeActivity {
    pub scope: String,
    pub commit_count: u32,
    pub rework_count: u32,
}

/// 单项目 Git 健康度指标。
#[derive(Debug, Serialize)]
pub struct ProjectCommitHealth {
    pub project_id: String,
    pub total_commits: u32,
    pub conventional_commits: u32,
    /// conventional / total（0.0 – 1.0，三位小数）
    pub conventional_rate: f64,
    pub rework_commits: u32,
    /// fix(非CI) / conventional（0.0 – 1.0，三位小数）
    pub rework_rate: f64,
    /// 按 commit 数降序的前 5 个 scope
    pub top_scopes: Vec<ScopeActivity>,
}

/// 内部聚合逻辑，接受 Connection 引用，方便测试直接调用。
pub fn project_commit_health_inner(
    conn: &rusqlite::Connection,
    project_ids: &[String],
) -> Result<Vec<ProjectCommitHealth>, String> {
    let round3 = |v: f64| (v * 1000.0).round() / 1000.0;
    let mut result = Vec::new();

    for pid in project_ids {
        let total: u32 = conn
            .query_row(
                "SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1",
                rusqlite::params![pid],
                |r| r.get::<_, i64>(0),
            )
            .unwrap_or(0) as u32;
        if total == 0 { continue; }

        let conventional: u32 = conn
            .query_row(
                "SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1 AND commit_type IS NOT NULL",
                rusqlite::params![pid],
                |r| r.get::<_, i64>(0),
            )
            .unwrap_or(0) as u32;

        let rework: u32 = conn
            .query_row(
                "SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1 AND is_rework=1 AND is_ci_auto=0",
                rusqlite::params![pid],
                |r| r.get::<_, i64>(0),
            )
            .unwrap_or(0) as u32;

        let mut stmt = conn
            .prepare(
                "SELECT scope,
                        COUNT(*) AS cnt,
                        SUM(CASE WHEN is_rework=1 AND is_ci_auto=0 THEN 1 ELSE 0 END) AS rc
                 FROM commit_metrics
                 WHERE project_id = ?1 AND scope IS NOT NULL
                 GROUP BY scope
                 ORDER BY cnt DESC
                 LIMIT 5",
            )
            .map_err(|e| e.to_string())?;
        let top_scopes: Vec<ScopeActivity> = stmt
            .query_map(rusqlite::params![pid], |row| {
                Ok(ScopeActivity {
                    scope: row.get(0)?,
                    commit_count: row.get::<_, i64>(1)? as u32,
                    rework_count: row.get::<_, i64>(2)? as u32,
                })
            })
            .map_err(|e| e.to_string())?
            .filter_map(|r| r.ok())
            .collect();

        result.push(ProjectCommitHealth {
            conventional_rate: round3(if total > 0 { conventional as f64 / total as f64 } else { 0.0 }),
            rework_rate: round3(if conventional > 0 { rework as f64 / conventional as f64 } else { 0.0 }),
            project_id: pid.clone(),
            total_commits: total,
            conventional_commits: conventional,
            rework_commits: rework,
            top_scopes,
        });
    }
    Ok(result)
}

/// 批量查询各项目的 Git 健康度（直接从 commit_metrics 表聚合，不经 usage.json）。
#[tauri::command]
pub fn get_project_commit_health(
    project_ids: Vec<String>,
) -> Result<Vec<ProjectCommitHealth>, String> {
    if project_ids.is_empty() {
        return Ok(vec![]);
    }
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    project_commit_health_inner(&conn, &project_ids)
}

/// commit_metrics 中所有项目的健康度（含项目名解析）。
#[derive(Debug, Serialize)]
pub struct CommitHealthWithName {
    pub project_id: String,
    /// 解析出的项目名：优先从 projects 表直接匹配，其次通过 blueprint_buffs 路径关联 projects，再次用路径末段
    pub project_name: String,
    pub total_commits: u32,
    pub conventional_commits: u32,
    pub conventional_rate: f64,
    pub rework_commits: u32,
    pub rework_rate: f64,
    pub top_scopes: Vec<ScopeActivity>,
}

/// 查询 commit_metrics 里所有有数据的项目，自动关联名称。无需前端传 project_ids。
#[tauri::command]
pub fn get_all_commit_health() -> Result<Vec<CommitHealthWithName>, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let round3 = |v: f64| (v * 1000.0).round() / 1000.0;

    // 聚合各 project_id 的指标，四条路径解析名称：
    // 1) projects 直接匹配（名称字符串 id）
    // 2) blueprint_buffs + projects 路径关联
    // 3) project_profiles "prof-UUID" 格式（孤儿 UUID 的名称来源）
    // 4) blueprint_buffs.project_path（后处理提取末段目录名）
    let mut stmt = conn.prepare(
        "SELECT
             cm.project_id,
             COALESCE(
                 p_direct.name,
                 p_via_buff.name,
                 pp_prof.name,
                 bb.project_path,
                 cm.project_id
             ) AS name,
             COUNT(*) AS total,
             SUM(CASE WHEN cm.commit_type IS NOT NULL THEN 1 ELSE 0 END) AS conventional,
             SUM(CASE WHEN cm.is_rework=1 AND cm.is_ci_auto=0 THEN 1 ELSE 0 END) AS rework
         FROM commit_metrics cm
         LEFT JOIN projects p_direct ON p_direct.id = cm.project_id
         LEFT JOIN blueprint_buffs bb ON bb.project_id = cm.project_id
         LEFT JOIN projects p_via_buff
               ON p_via_buff.win_path = bb.project_path
               OR p_via_buff.wsl_path = bb.project_path
         LEFT JOIN project_profiles pp_prof ON pp_prof.id = ('prof-' || cm.project_id)
         GROUP BY cm.project_id
         ORDER BY total DESC",
    ).map_err(|e| e.to_string())?;

    let rows: Vec<(String, String, u32, u32, u32)> = stmt
        .query_map([], |row| {
            Ok((
                row.get::<_, String>(0)?,
                row.get::<_, String>(1)?,
                row.get::<_, i64>(2)? as u32,
                row.get::<_, i64>(3)? as u32,
                row.get::<_, i64>(4)? as u32,
            ))
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    let mut result = Vec::with_capacity(rows.len());
    for (pid, name, total, conventional, rework) in rows {
        // 查 top_scopes
        let mut scope_stmt = conn.prepare(
            "SELECT scope,
                    COUNT(*) AS cnt,
                    SUM(CASE WHEN is_rework=1 AND is_ci_auto=0 THEN 1 ELSE 0 END) AS rc
             FROM commit_metrics
             WHERE project_id = ?1 AND scope IS NOT NULL
             GROUP BY scope
             ORDER BY cnt DESC
             LIMIT 5",
        ).map_err(|e| e.to_string())?;
        let top_scopes: Vec<ScopeActivity> = scope_stmt
            .query_map(rusqlite::params![pid], |row| {
                Ok(ScopeActivity {
                    scope: row.get(0)?,
                    commit_count: row.get::<_, i64>(1)? as u32,
                    rework_count: row.get::<_, i64>(2)? as u32,
                })
            })
            .map_err(|e| e.to_string())?
            .filter_map(|r| r.ok())
            .collect();

        // 如果 name 解析为路径（含 / 或 \），取末段目录名
        let display_name = if name.contains('/') || name.contains('\\') {
            name.split(['/', '\\']).filter(|s| !s.is_empty()).last()
                .unwrap_or(&name)
                .to_string()
        } else {
            name
        };

        result.push(CommitHealthWithName {
            conventional_rate: round3(if total > 0 { conventional as f64 / total as f64 } else { 0.0 }),
            rework_rate: round3(if conventional > 0 { rework as f64 / conventional as f64 } else { 0.0 }),
            project_id: pid,
            project_name: display_name,
            total_commits: total,
            conventional_commits: conventional,
            rework_commits: rework,
            top_scopes,
        });
    }
    Ok(result)
}

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

    #[test]
    fn parses_feat_with_scope() {
        let p = parse_conventional("feat(hub/rpc): add shared type system");
        assert_eq!(p.commit_type.as_deref(), Some("feat"));
        assert_eq!(p.scope.as_deref(), Some("hub/rpc"));
        assert!(!p.is_rework);
        assert!(!p.is_ci_auto);
    }

    #[test]
    fn parses_fix_as_rework() {
        let p = parse_conventional("fix(estate1/view): correct sort order");
        assert_eq!(p.commit_type.as_deref(), Some("fix"));
        assert_eq!(p.scope.as_deref(), Some("estate1/view"));
        assert!(p.is_rework);
        assert!(!p.is_ci_auto);
    }

    #[test]
    fn parses_type_without_scope() {
        let p = parse_conventional("chore: update AGENTS.md");
        assert_eq!(p.commit_type.as_deref(), Some("chore"));
        assert_eq!(p.scope, None);
        assert!(!p.is_rework);
    }

    #[test]
    fn detects_ci_auto_fix() {
        let p = parse_conventional("fix(ci): auto-fix type/lint errors [DeepSeek-V3]");
        assert_eq!(p.commit_type.as_deref(), Some("fix"));
        assert!(p.is_rework);
        assert!(p.is_ci_auto, "应识别 [DeepSeek-V3] 为 CI 自修");
    }

    #[test]
    fn breaking_change_marker_stripped() {
        let p = parse_conventional("feat(api)!: drop legacy endpoint");
        assert_eq!(p.commit_type.as_deref(), Some("feat"));
        assert_eq!(p.scope.as_deref(), Some("api"));
    }

    #[test]
    fn merge_commit_is_non_conventional() {
        let p = parse_conventional("Merge pull request 'xxx' (#1) from dev into main");
        assert_eq!(p.commit_type, None);
        assert!(!p.is_rework);
    }

    #[test]
    fn freeform_message_is_non_conventional() {
        let p = parse_conventional("update stuff");
        assert_eq!(p.commit_type, None);
        assert_eq!(p.scope, None);
    }

    #[test]
    fn uses_first_line_only() {
        let p = parse_conventional("feat(x): title\n\nbody line with: colon");
        assert_eq!(p.commit_type.as_deref(), Some("feat"));
        assert_eq!(p.scope.as_deref(), Some("x"));
    }

    #[test]
    fn rework_count_excludes_ci_auto_and_non_fix() {
        let conn = crate::db::conn_with_schema();
        record_commit(&conn, "proj", "sha1", "fix(a): real bug", None).unwrap();
        record_commit(&conn, "proj", "sha2", "fix(ci): auto-fix [DeepSeek-V3]", None).unwrap();
        record_commit(&conn, "proj", "sha3", "feat(a): feature", None).unwrap();
        record_commit(&conn, "proj", "sha4", "update stuff", None).unwrap();
        // 仅 sha1 算有效返工（sha2 是 CI 自修、sha3 是 feat、sha4 非规范）
        assert_eq!(rework_count(&conn, "proj").unwrap(), 1);
    }

    #[test]
    fn record_commit_is_idempotent() {
        let conn = crate::db::conn_with_schema();
        record_commit(&conn, "proj", "dup", "fix: x", None).unwrap();
        record_commit(&conn, "proj", "dup", "fix: x", None).unwrap();
        let (total, _) = conventional_stats(&conn, "proj").unwrap();
        assert_eq!(total, 1, "同 sha 重复写入应被忽略");
    }

    #[test]
    fn conventional_stats_counts_ratio() {
        let conn = crate::db::conn_with_schema();
        record_commit(&conn, "proj", "s1", "feat: a", None).unwrap();
        record_commit(&conn, "proj", "s2", "random text", None).unwrap();
        let (total, conv) = conventional_stats(&conn, "proj").unwrap();
        assert_eq!(total, 2);
        assert_eq!(conv, 1);
    }

    // ── F1 新增测试 ────────────────────────────────────────────────────────────

    /// 辅助：向 in-memory DB 写入一批 commits
    fn seed_commits(conn: &rusqlite::Connection, pid: &str, msgs: &[(&str, &str)]) {
        for (sha, msg) in msgs {
            record_commit(conn, pid, sha, msg, None).unwrap();
        }
    }

    #[test]
    fn health_empty_project_excluded() {
        let conn = crate::db::conn_with_schema();
        // project_id 没有任何 commit，inner 函数应直接跳过返回空
        let result = project_commit_health_inner(&conn, &["ghost-proj".to_string()]).unwrap();
        assert!(result.is_empty(), "无数据项目应被跳过");
    }

    #[test]
    fn health_aggregates_correctly() {
        let conn = crate::db::conn_with_schema();
        seed_commits(&conn, "p1", &[
            ("a1", "feat(api): add endpoint"),
            ("a2", "fix(api): correct response"),      // rework
            ("a3", "fix(ci): auto-fix [DeepSeek-V3]"), // CI，不计返工
            ("a4", "chore: update deps"),
            ("a5", "random commit"),                   // 非 conventional
        ]);
        let result = project_commit_health_inner(&conn, &["p1".to_string()]).unwrap();
        let h = &result[0];
        assert_eq!(h.total_commits, 5);
        assert_eq!(h.conventional_commits, 4); // a1/a2/a3/a4
        assert_eq!(h.rework_commits, 1);        // 只有 a2（a3 是 CI）
        assert!((h.conventional_rate - 0.8).abs() < 0.001);
        assert!((h.rework_rate - 0.25).abs() < 0.001); // 1/4 conventional
    }

    #[test]
    fn health_top_scopes_ordered_by_count() {
        let conn = crate::db::conn_with_schema();
        seed_commits(&conn, "p2", &[
            ("b1", "feat(api): x"),
            ("b2", "feat(api): y"),
            ("b3", "feat(api): z"),
            ("b4", "fix(db): bug"),
            ("b5", "chore(db): cleanup"),
        ]);
        let result = project_commit_health_inner(&conn, &["p2".to_string()]).unwrap();
        let h = &result[0];
        assert_eq!(h.top_scopes[0].scope, "api");
        assert_eq!(h.top_scopes[0].commit_count, 3);
        assert_eq!(h.top_scopes[1].scope, "db");
        assert_eq!(h.top_scopes[1].commit_count, 2);
    }

    #[test]
    fn ingest_idempotent_via_record_commit() {
        let conn = crate::db::conn_with_schema();
        record_commit(&conn, "p3", "sha-dup", "feat: first write", None).unwrap();
        record_commit(&conn, "p3", "sha-dup", "feat: second write", None).unwrap();
        let (total, _) = conventional_stats(&conn, "p3").unwrap();
        assert_eq!(total, 1, "重复 sha 应被 INSERT OR IGNORE 忽略");
    }

    // ── R1：Rules-Applied trailer ──────────────────────────────────────────

    #[test]
    fn parse_rules_applied_normalizes_tokens() {
        let msg = "feat(core): thing

说明

Rules-Applied: r06, R01,R01, xx, R10";
        assert_eq!(parse_rules_applied(msg).as_deref(), Some("R01,R06,R10"));
    }

    #[test]
    fn parse_rules_applied_absent_or_empty() {
        assert_eq!(parse_rules_applied("feat: no trailer"), None);
        assert_eq!(parse_rules_applied("Rules-Applied: 无效, abc"), None);
    }

    #[test]
    fn record_commit_stores_rules_applied() {
        let conn = crate::db::conn_with_schema();
        record_commit(&conn, "pr", "sha-r1", "feat(x): a

Rules-Applied: R01,R05", None).unwrap();
        let stored: Option<String> = conn.query_row(
            "SELECT rules_applied FROM commit_metrics WHERE sha = 'sha-r1'", [], |r| r.get(0),
        ).unwrap();
        assert_eq!(stored.as_deref(), Some("R01,R05"));
    }

    #[test]
    fn rule_hit_stats_cross_counts_rework() {
        let conn = crate::db::conn_with_schema();
        record_commit(&conn, "pa", "s1", "feat(x): ok

Rules-Applied: R01", None).unwrap();
        record_commit(&conn, "pa", "s2", "fix(x): broke

Rules-Applied: R01,R06", None).unwrap();
        record_commit(&conn, "pb", "s3", "feat(y): fine

Rules-Applied: R06", None).unwrap();
        // CI 自修的 fix 不计返工
        record_commit(&conn, "pb", "s4", "fix(y): auto [DeepSeek-V3]

Rules-Applied: R06", None).unwrap();

        let stats = rule_hit_stats(&conn).unwrap();
        let r01 = stats.iter().find(|s| s.rule_id == "R01").unwrap();
        assert_eq!(r01.hits, 2);
        assert_eq!(r01.rework_hits, 1);
        assert_eq!(r01.project_count, 1);
        let r06 = stats.iter().find(|s| s.rule_id == "R06").unwrap();
        assert_eq!(r06.hits, 3);
        assert_eq!(r06.rework_hits, 1, "CI 自修 fix 不应计入返工");
        assert_eq!(r06.project_count, 2);
    }

    #[test]
    fn rule_hit_stats_empty_when_no_trailers() {
        let conn = crate::db::conn_with_schema();
        record_commit(&conn, "pz", "s9", "feat: plain", None).unwrap();
        assert!(rule_hit_stats(&conn).unwrap().is_empty());
    }
}

