//! Gitea 集成——控制面：实例管理 + 仓库操作 + webhook 注册。
//!
//! 观测面（webhook 接收端）在 mcp_server.rs 实现。

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

// ── 数据结构 ──────────────────────────────────────────────────────────────────

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GiteaInstance {
    pub id: String,
    pub name: String,
    pub url: String,
    pub access_token: String,
    pub is_default: bool,
    pub created_at: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct SaveGiteaInstanceInput {
    pub name: String,
    pub url: String,
    pub access_token: String,
    pub is_default: Option<bool>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GiteaRepo {
    pub id: String,
    pub instance_id: String,
    pub project_id: String,
    pub gitea_owner: String,
    pub gitea_repo_name: String,
    pub clone_url: Option<String>,
    pub webhook_secret: Option<String>,
    pub webhook_id: Option<i64>,
    pub linked_at: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct GiteaUser {
    pub login: String,
    pub full_name: String,
    pub email: String,
}

// ── 辅助函数 ──────────────────────────────────────────────────────────────────

fn row_to_instance(row: &rusqlite::Row<'_>) -> rusqlite::Result<GiteaInstance> {
    Ok(GiteaInstance {
        id:           row.get(0)?,
        name:         row.get(1)?,
        url:          row.get(2)?,
        access_token: row.get(3)?,
        is_default:   row.get::<_, i64>(4)? != 0,
        created_at:   row.get(5)?,
    })
}

fn row_to_repo(row: &rusqlite::Row<'_>) -> rusqlite::Result<GiteaRepo> {
    Ok(GiteaRepo {
        id:              row.get(0)?,
        instance_id:     row.get(1)?,
        project_id:      row.get(2)?,
        gitea_owner:     row.get(3)?,
        gitea_repo_name: row.get(4)?,
        clone_url:       row.get(5)?,
        webhook_secret:  row.get(6)?,
        webhook_id:      row.get(7)?,
        linked_at:       row.get(8)?,
    })
}

/// 规范化 Gitea URL：去掉末尾 /
fn normalize_url(url: &str) -> String {
    url.trim_end_matches('/').to_string()
}

// ── 实例管理命令 ──────────────────────────────────────────────────────────────

/// 保存 Gitea 实例（新增或覆盖同名实例）
#[tauri::command]
pub fn gitea_save_instance(input: SaveGiteaInstanceInput) -> Result<GiteaInstance, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let id = uuid::Uuid::new_v4().to_string();
    let url = normalize_url(&input.url);
    let is_default = input.is_default.unwrap_or(false) as i64;

    // 若设为默认，先取消其他默认
    if is_default != 0 {
        let _ = conn.execute("UPDATE gitea_instances SET is_default = 0", []);
    }

    conn.execute(
        "INSERT INTO gitea_instances (id, name, url, access_token, is_default)
         VALUES (?1, ?2, ?3, ?4, ?5)",
        params![id, input.name, url, input.access_token, is_default],
    ).map_err(|e| e.to_string())?;

    conn.query_row(
        "SELECT id, name, url, access_token, is_default, created_at FROM gitea_instances WHERE id = ?1",
        params![id],
        row_to_instance,
    ).map_err(|e| e.to_string())
}

/// 列出所有 Gitea 实例
#[tauri::command]
pub fn gitea_list_instances() -> Result<Vec<GiteaInstance>, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, name, url, access_token, is_default, created_at FROM gitea_instances ORDER BY is_default DESC, created_at ASC"
    ).map_err(|e| e.to_string())?;
    let rows = stmt.query_map([], row_to_instance)
        .map_err(|e| e.to_string())?
        .collect::<rusqlite::Result<Vec<_>>>()
        .map_err(|e| e.to_string())?;
    Ok(rows)
}

/// 删除 Gitea 实例（级联删除 gitea_repos）
#[tauri::command]
pub fn gitea_delete_instance(id: String) -> Result<(), String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM gitea_instances WHERE id = ?1", params![id])
        .map_err(|e| e.to_string())?;
    Ok(())
}

/// 更新 Gitea 实例（仅更新非空字段）
#[tauri::command]
pub fn gitea_update_instance(
    id: String,
    name: Option<String>,
    url: Option<String>,
    access_token: Option<String>,
    is_default: Option<bool>,
) -> Result<GiteaInstance, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;

    if let Some(ref _n) = name {
        conn.execute("UPDATE gitea_instances SET name = ?1 WHERE id = ?2", params![_n, id])
            .map_err(|e| e.to_string())?;
    }
    if let Some(ref u) = url {
        let u = normalize_url(u);
        conn.execute("UPDATE gitea_instances SET url = ?1 WHERE id = ?2", params![u, id])
            .map_err(|e| e.to_string())?;
    }
    if let Some(ref t) = access_token {
        conn.execute("UPDATE gitea_instances SET access_token = ?1 WHERE id = ?2", params![t, id])
            .map_err(|e| e.to_string())?;
    }
    if let Some(def) = is_default {
        if def {
            let _ = conn.execute("UPDATE gitea_instances SET is_default = 0", []);
        }
        conn.execute("UPDATE gitea_instances SET is_default = ?1 WHERE id = ?2",
            params![def as i64, id])
            .map_err(|e| e.to_string())?;
    }

    conn.query_row(
        "SELECT id, name, url, access_token, is_default, created_at FROM gitea_instances WHERE id = ?1",
        params![id],
        row_to_instance,
    ).map_err(|e| e.to_string())
}

/// 测试 Gitea 连接：调 GET /api/v1/user，返回用户信息验证 token 有效性
#[tauri::command]
pub fn gitea_test_connection(instance_id: String) -> Result<GiteaUser, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let (url, token): (String, String) = conn.query_row(
        "SELECT url, access_token FROM gitea_instances WHERE id = ?1",
        params![instance_id],
        |r| Ok((r.get(0)?, r.get(1)?)),
    ).map_err(|_| "实例不存在".to_string())?;

    gitea_api_get_user(&url, &token)
}

// ── Gitea API 辅助 ────────────────────────────────────────────────────────────

/// 读取实例的 (url, token)
fn instance_auth(conn: &rusqlite::Connection, instance_id: &str) -> Result<(String, String), String> {
    conn.query_row(
        "SELECT url, access_token FROM gitea_instances WHERE id = ?1",
        params![instance_id],
        |r| Ok((r.get(0)?, r.get(1)?)),
    ).map_err(|_| "Gitea 实例不存在".to_string())
}

/// 把 ureq 错误转成可读信息（尽量提取 Gitea 返回的 message 字段）
fn api_err(prefix: &str, e: ureq::Error) -> String {
    match e {
        ureq::Error::Status(code, resp) => {
            let msg = resp.into_json::<serde_json::Value>().ok()
                .and_then(|v| v["message"].as_str().map(|s| s.to_string()))
                .unwrap_or_default();
            if msg.is_empty() {
                format!("{prefix}: HTTP {code}")
            } else {
                format!("{prefix}: {msg}（HTTP {code}）")
            }
        }
        other => format!("{prefix}: {other}"),
    }
}

/// 写入或更新项目 ↔ 仓库绑定（换绑时重置 webhook 字段，旧 hook 已随旧仓库失效）
fn upsert_repo_link(
    conn: &rusqlite::Connection,
    instance_id: &str,
    project_id: &str,
    owner: &str,
    repo_name: &str,
    clone_url: Option<&str>,
) -> Result<GiteaRepo, String> {
    let id = uuid::Uuid::new_v4().to_string();
    conn.execute(
        "INSERT INTO gitea_repos (id, instance_id, project_id, gitea_owner, gitea_repo_name, clone_url)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6)
         ON CONFLICT(project_id) DO UPDATE SET
            instance_id = ?2, gitea_owner = ?4, gitea_repo_name = ?5, clone_url = ?6,
            webhook_secret = NULL, webhook_id = NULL",
        params![id, instance_id, project_id, owner, repo_name, clone_url],
    ).map_err(|e| e.to_string())?;

    conn.query_row(
        "SELECT id, instance_id, project_id, gitea_owner, gitea_repo_name,
                clone_url, webhook_secret, webhook_id, linked_at
         FROM gitea_repos WHERE project_id = ?1",
        params![project_id],
        row_to_repo,
    ).map_err(|e| e.to_string())
}

/// GET /api/v1/user — 验证 token 并返回用户信息
pub fn gitea_api_get_user(base_url: &str, token: &str) -> Result<GiteaUser, String> {
    let url = format!("{}/api/v1/user", base_url);
    let resp = ureq::get(&url)
        .set("Authorization", &format!("token {token}"))
        .set("Content-Type", "application/json")
        .call()
        .map_err(|e| format!("Gitea 连接失败: {e}"))?;

    let json: serde_json::Value = resp.into_json()
        .map_err(|e| format!("解析响应失败: {e}"))?;

    let login = json["login"].as_str()
        .ok_or_else(|| "token 无效或权限不足".to_string())?
        .to_string();

    Ok(GiteaUser {
        login,
        full_name: json["full_name"].as_str().unwrap_or("").to_string(),
        email:     json["email"].as_str().unwrap_or("").to_string(),
    })
}

/// GET /api/v1/repos/:owner/:repo — 获取仓库信息（G2 仓库绑定时使用）
pub fn gitea_api_get_repo(
    base_url: &str,
    token: &str,
    owner: &str,
    repo: &str,
) -> Result<serde_json::Value, String> {
    let url = format!("{}/api/v1/repos/{owner}/{repo}", base_url);
    let resp = ureq::get(&url)
        .set("Authorization", &format!("token {token}"))
        .call()
        .map_err(|e| api_err("获取仓库信息失败", e))?;
    resp.into_json().map_err(|e| format!("解析失败: {e}"))
}

// ── 仓库创建 / 迁移 / 绑定（G2）──────────────────────────────────────────────

/// 在 Gitea 新建仓库并绑定到项目
#[tauri::command]
pub fn gitea_create_repo(
    instance_id: String,
    project_id: String,
    repo_name: String,
    description: Option<String>,
    private: Option<bool>,
) -> Result<GiteaRepo, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let (base_url, token) = instance_auth(&conn, &instance_id)?;

    let resp = ureq::post(&format!("{base_url}/api/v1/user/repos"))
        .set("Authorization", &format!("token {token}"))
        .send_json(serde_json::json!({
            "name": repo_name,
            "description": description.unwrap_or_default(),
            "private": private.unwrap_or(true),
            "auto_init": false,
        }))
        .map_err(|e| api_err("创建仓库失败", e))?;

    let json: serde_json::Value = resp.into_json().map_err(|e| format!("解析响应失败: {e}"))?;
    let owner = json["owner"]["login"].as_str().ok_or("响应缺少 owner 字段")?.to_string();
    let clone_url = json["clone_url"].as_str().map(|s| s.to_string());

    upsert_repo_link(&conn, &instance_id, &project_id, &owner, &repo_name, clone_url.as_deref())
}

/// 从 GitHub/GitLab 等迁移仓库到 Gitea 并绑定到项目
#[tauri::command]
pub fn gitea_migrate_repo(
    instance_id: String,
    project_id: String,
    clone_addr: String,
    repo_name: String,
    auth_token: Option<String>,
    private: Option<bool>,
) -> Result<GiteaRepo, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let (base_url, token) = instance_auth(&conn, &instance_id)?;

    let mut body = serde_json::json!({
        "clone_addr": clone_addr,
        "repo_name": repo_name,
        "private": private.unwrap_or(true),
        "mirror": false,
        "service": "git",
    });
    if let Some(t) = auth_token.filter(|t| !t.trim().is_empty()) {
        body["auth_token"] = serde_json::Value::String(t);
    }

    let resp = ureq::post(&format!("{base_url}/api/v1/repos/migrate"))
        .set("Authorization", &format!("token {token}"))
        .send_json(body)
        .map_err(|e| api_err("迁移仓库失败", e))?;

    let json: serde_json::Value = resp.into_json().map_err(|e| format!("解析响应失败: {e}"))?;
    let owner = json["owner"]["login"].as_str().ok_or("响应缺少 owner 字段")?.to_string();
    let clone_url = json["clone_url"].as_str().map(|s| s.to_string());

    upsert_repo_link(&conn, &instance_id, &project_id, &owner, &repo_name, clone_url.as_deref())
}

/// 绑定 Gitea 上已存在的仓库到项目（先验证仓库存在）
#[tauri::command]
pub fn gitea_link_repo(
    instance_id: String,
    project_id: String,
    owner: String,
    repo_name: String,
) -> Result<GiteaRepo, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let (base_url, token) = instance_auth(&conn, &instance_id)?;

    let json = gitea_api_get_repo(&base_url, &token, &owner, &repo_name)?;
    let clone_url = json["clone_url"].as_str().map(|s| s.to_string());

    upsert_repo_link(&conn, &instance_id, &project_id, &owner, &repo_name, clone_url.as_deref())
}

// ── Webhook 注册 / 解绑（G4）─────────────────────────────────────────────────

/// 为项目绑定的仓库注册 push webhook。
/// callback_base 形如 http://192.168.1.5:27190（Gitea 服务器视角可达的宿主机地址）。
/// 已有 webhook 时先删旧再建新（幂等重设）。
#[tauri::command]
pub fn gitea_setup_webhook(project_id: String, callback_base: String) -> Result<GiteaRepo, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let link = conn.query_row(
        "SELECT id, instance_id, project_id, gitea_owner, gitea_repo_name,
                clone_url, webhook_secret, webhook_id, linked_at
         FROM gitea_repos WHERE project_id = ?1",
        params![project_id],
        row_to_repo,
    ).map_err(|_| "项目尚未绑定 Gitea 仓库".to_string())?;
    let (base_url, token) = instance_auth(&conn, &link.instance_id)?;

    // 旧 hook 存在则先删（失败不阻断，可能已在 Gitea 侧被手动删除）
    if let Some(old_id) = link.webhook_id {
        let _ = ureq::delete(&format!(
            "{base_url}/api/v1/repos/{}/{}/hooks/{old_id}",
            link.gitea_owner, link.gitea_repo_name
        ))
        .set("Authorization", &format!("token {token}"))
        .call();
    }

    let secret = format!("{}{}", uuid::Uuid::new_v4().simple(), uuid::Uuid::new_v4().simple());
    let callback_url = format!(
        "{}/webhook/gitea/{}",
        callback_base.trim_end_matches('/'),
        project_id
    );

    let resp = ureq::post(&format!(
        "{base_url}/api/v1/repos/{}/{}/hooks",
        link.gitea_owner, link.gitea_repo_name
    ))
    .set("Authorization", &format!("token {token}"))
    .send_json(serde_json::json!({
        "type": "gitea",
        "active": true,
        "branch_filter": "*",
        "events": ["push"],
        "config": {
            "url": callback_url,
            "content_type": "json",
            "secret": secret,
            "http_method": "post",
        },
    }))
    .map_err(|e| api_err("注册 webhook 失败", e))?;

    let json: serde_json::Value = resp.into_json().map_err(|e| format!("解析响应失败: {e}"))?;
    let hook_id = json["id"].as_i64().ok_or("响应缺少 hook id")?;

    conn.execute(
        "UPDATE gitea_repos SET webhook_secret = ?1, webhook_id = ?2 WHERE project_id = ?3",
        params![secret, hook_id, project_id],
    ).map_err(|e| e.to_string())?;

    conn.query_row(
        "SELECT id, instance_id, project_id, gitea_owner, gitea_repo_name,
                clone_url, webhook_secret, webhook_id, linked_at
         FROM gitea_repos WHERE project_id = ?1",
        params![project_id],
        row_to_repo,
    ).map_err(|e| e.to_string())
}

/// 解绑项目与 Gitea 仓库（顺带删除 Gitea 侧 webhook，失败不阻断）
#[tauri::command]
pub fn gitea_unlink_repo(project_id: String) -> Result<(), String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let link = match conn.query_row(
        "SELECT id, instance_id, project_id, gitea_owner, gitea_repo_name,
                clone_url, webhook_secret, webhook_id, linked_at
         FROM gitea_repos WHERE project_id = ?1",
        params![project_id],
        row_to_repo,
    ) {
        Ok(l) => l,
        Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(()),
        Err(e) => return Err(e.to_string()),
    };

    if let Some(hook_id) = link.webhook_id {
        if let Ok((base_url, token)) = instance_auth(&conn, &link.instance_id) {
            let _ = ureq::delete(&format!(
                "{base_url}/api/v1/repos/{}/{}/hooks/{hook_id}",
                link.gitea_owner, link.gitea_repo_name
            ))
            .set("Authorization", &format!("token {token}"))
            .call();
        }
    }

    conn.execute("DELETE FROM gitea_repos WHERE project_id = ?1", params![project_id])
        .map_err(|e| e.to_string())?;
    Ok(())
}

// ── 远端轮询采集（G6：webhook 不可达时的降级路径）────────────────────────────

/// 从 Gitea commits API 响应解析出 (sha, message, date) 列表（纯函数，可测）
pub fn parse_gitea_commits(json: &serde_json::Value) -> Vec<(String, String, Option<String>)> {
    json.as_array()
        .map(|arr| {
            arr.iter()
                .filter_map(|c| {
                    let sha = c["sha"].as_str()?.to_string();
                    let message = c["commit"]["message"].as_str().unwrap_or("").to_string();
                    let date = c["commit"]["committer"]["date"]
                        .as_str()
                        .or_else(|| c["created"].as_str())
                        .map(|s| s.to_string());
                    Some((sha, message, date))
                })
                .collect()
        })
        .unwrap_or_default()
}

/// 轮询所有绑定仓库的最新 commit，幂等写入 commit_metrics。
/// 返回 (轮询仓库数, 新写入 commit 数)。纯出站 HTTPS，适配公网 Gitea + 内网炼境拓扑。
pub fn poll_all_repos() -> Result<(usize, usize), String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let repos: Vec<(String, String, String, String, String)> = {
        let mut stmt = conn.prepare(
            "SELECT gr.project_id, gr.gitea_owner, gr.gitea_repo_name, gi.url, gi.access_token
             FROM gitea_repos gr JOIN gitea_instances gi ON gi.id = gr.instance_id",
        ).map_err(|e| e.to_string())?;
        let rows = stmt.query_map([], |r| {
            Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?))
        }).map_err(|e| e.to_string())?;
        rows.collect::<rusqlite::Result<Vec<_>>>().map_err(|e| e.to_string())?
    };

    let mut ingested = 0usize;
    let mut polled = 0usize;
    for (project_id, owner, repo, base_url, token) in &repos {
        let url = format!("{base_url}/api/v1/repos/{owner}/{repo}/commits?limit=50&stat=false&verification=false");
        let resp = match ureq::get(&url)
            .set("Authorization", &format!("token {token}"))
            .call()
        {
            Ok(r) => r,
            Err(e) => {
                crate::db::log_event("gitea_poll", "warn",
                    &format!("轮询 {owner}/{repo} 失败: {}", api_err("拉取 commits", e)), None);
                continue;
            }
        };
        let json: serde_json::Value = match resp.into_json() {
            Ok(j) => j,
            Err(_) => continue,
        };
        polled += 1;
        for (sha, message, date) in parse_gitea_commits(&json) {
            let before: i64 = conn.query_row(
                "SELECT COUNT(*) FROM commit_metrics WHERE sha = ?1", params![sha], |r| r.get(0),
            ).unwrap_or(0);
            if before == 0
                && crate::commands::commit_metrics::record_commit(
                    &conn, project_id, &sha, &message, date.as_deref(),
                ).is_ok()
            {
                ingested += 1;
            }
        }
    }

    if ingested > 0 {
        crate::db::log_event("gitea_poll", "info",
            &format!("轮询 {polled} 个仓库，新采集 {ingested} 条 commit"), None);
    }
    Ok((polled, ingested))
}

/// 手动触发一次轮询（UI「立即拉取」按钮）
#[tauri::command]
pub fn gitea_poll_now() -> Result<serde_json::Value, String> {
    let (polled, ingested) = poll_all_repos()?;
    Ok(serde_json::json!({ "polled": polled, "ingested": ingested }))
}

// ── 仓库绑定查询命令 ──────────────────────────────────────────────────────────

/// 查询所有已绑定的仓库（含实例名）
#[tauri::command]
pub fn gitea_list_linked_repos() -> Result<Vec<serde_json::Value>, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT gr.id, gr.instance_id, gi.name as instance_name, gi.url as instance_url,
                gr.project_id, gr.gitea_owner, gr.gitea_repo_name,
                gr.clone_url, gr.webhook_secret, gr.webhook_id, gr.linked_at
         FROM gitea_repos gr
         JOIN gitea_instances gi ON gi.id = gr.instance_id
         ORDER BY gr.linked_at DESC",
    ).map_err(|e| e.to_string())?;

    let rows = stmt.query_map([], |row| {
        Ok(serde_json::json!({
            "id":              row.get::<_, String>(0)?,
            "instance_id":     row.get::<_, String>(1)?,
            "instance_name":   row.get::<_, String>(2)?,
            "instance_url":    row.get::<_, String>(3)?,
            "project_id":      row.get::<_, String>(4)?,
            "gitea_owner":     row.get::<_, String>(5)?,
            "gitea_repo_name": row.get::<_, String>(6)?,
            "clone_url":       row.get::<_, Option<String>>(7)?,
            "has_webhook":     row.get::<_, Option<String>>(8)?.is_some(),
            "webhook_id":      row.get::<_, Option<i64>>(9)?,
            "linked_at":       row.get::<_, Option<String>>(10)?,
        }))
    }).map_err(|e| e.to_string())?
    .collect::<rusqlite::Result<Vec<_>>>()
    .map_err(|e| e.to_string())?;

    Ok(rows)
}

/// 查询单个项目的 Gitea 绑定
#[tauri::command]
pub fn gitea_get_repo_link(project_id: String) -> Result<Option<GiteaRepo>, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    match conn.query_row(
        "SELECT id, instance_id, project_id, gitea_owner, gitea_repo_name,
                clone_url, webhook_secret, webhook_id, linked_at
         FROM gitea_repos WHERE project_id = ?1",
        params![project_id],
        row_to_repo,
    ) {
        Ok(r) => Ok(Some(r)),
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
        Err(e) => Err(e.to_string()),
    }
}

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

    #[test]
    fn normalize_url_strips_trailing_slash() {
        assert_eq!(normalize_url("https://gitea.example.com/"), "https://gitea.example.com");
        assert_eq!(normalize_url("https://gitea.example.com"), "https://gitea.example.com");
        assert_eq!(normalize_url("https://gitea.example.com///"), "https://gitea.example.com");
    }

    #[test]
    fn gitea_tables_exist_after_migrate() {
        let conn = crate::db::conn_with_schema();
        let count: i64 = conn.query_row(
            "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('gitea_instances','gitea_repos')",
            [],
            |r| r.get(0),
        ).unwrap();
        assert_eq!(count, 2, "gitea_instances 或 gitea_repos 表未建");
    }

    #[test]
    fn save_and_list_instance() {
        let conn = crate::db::conn_with_schema();
        conn.execute(
            "INSERT INTO gitea_instances (id, name, url, access_token, is_default)
             VALUES ('inst-1', '测试实例', 'https://gitea.test', 'token123', 0)",
            [],
        ).unwrap();

        let count: i64 = conn.query_row(
            "SELECT COUNT(*) FROM gitea_instances WHERE id = 'inst-1'",
            [],
            |r| r.get(0),
        ).unwrap();
        assert_eq!(count, 1);
    }

    #[test]
    fn upsert_repo_link_resets_webhook_on_rebind() {
        let conn = crate::db::conn_with_schema();
        conn.execute(
            "INSERT INTO gitea_instances (id, name, url, access_token) VALUES ('i1', 'a', 'u', 't')",
            [],
        ).unwrap();

        // 首次绑定
        let r1 = upsert_repo_link(&conn, "i1", "proj-1", "alice", "repo-a", Some("https://g/a.git")).unwrap();
        assert_eq!(r1.gitea_repo_name, "repo-a");

        // 模拟已设置 webhook
        conn.execute(
            "UPDATE gitea_repos SET webhook_secret = 'sec', webhook_id = 42 WHERE project_id = 'proj-1'",
            [],
        ).unwrap();

        // 换绑到另一个仓库 → webhook 字段应被重置
        let r2 = upsert_repo_link(&conn, "i1", "proj-1", "alice", "repo-b", None).unwrap();
        assert_eq!(r2.gitea_repo_name, "repo-b");
        assert!(r2.webhook_secret.is_none(), "换绑后 webhook_secret 未重置");
        assert!(r2.webhook_id.is_none(), "换绑后 webhook_id 未重置");

        // 仍然只有一行
        let count: i64 = conn.query_row(
            "SELECT COUNT(*) FROM gitea_repos WHERE project_id = 'proj-1'", [], |r| r.get(0),
        ).unwrap();
        assert_eq!(count, 1);
    }

    #[test]
    fn unique_index_on_project_id() {
        let conn = crate::db::conn_with_schema();
        conn.execute(
            "INSERT INTO gitea_instances (id, name, url, access_token) VALUES ('i1', 'a', 'u', 't')",
            [],
        ).unwrap();
        conn.execute(
            "INSERT INTO gitea_repos (id, instance_id, project_id, gitea_owner, gitea_repo_name)
             VALUES ('r1', 'i1', 'proj-uuid-1', 'owner', 'repo')",
            [],
        ).unwrap();
        // 同一 project_id 第二次应失败
        let result = conn.execute(
            "INSERT INTO gitea_repos (id, instance_id, project_id, gitea_owner, gitea_repo_name)
             VALUES ('r2', 'i1', 'proj-uuid-1', 'owner', 'repo2')",
            [],
        );
        assert!(result.is_err(), "project_id 唯一索引未生效");
    }

    // ── G6：轮询解析 ──────────────────────────────────────────────────────

    #[test]
    fn parse_gitea_commits_extracts_fields() {
        let json = serde_json::json!([
            {"sha": "abc", "commit": {"message": "feat(x): a", "committer": {"date": "2026-07-02T10:00:00+08:00"}}},
            {"sha": "def", "commit": {"message": "fix: b", "committer": {}}, "created": "2026-07-01T09:00:00+08:00"}
        ]);
        let commits = parse_gitea_commits(&json);
        assert_eq!(commits.len(), 2);
        assert_eq!(commits[0].0, "abc");
        assert_eq!(commits[0].2.as_deref(), Some("2026-07-02T10:00:00+08:00"));
        assert_eq!(commits[1].2.as_deref(), Some("2026-07-01T09:00:00+08:00"), "committer.date 缺失时回退 created");
    }

    #[test]
    fn parse_gitea_commits_tolerates_garbage() {
        assert!(parse_gitea_commits(&serde_json::json!({"not": "array"})).is_empty());
        assert!(parse_gitea_commits(&serde_json::json!([{"no_sha": true}])).is_empty());
    }
}

