//! 应用错误类型与 `ServerFnError` 转换。
//!
//! `AppError` 封装认证、权限、数据库、内部错误等场景，
//! 并转换为对外友好的 `ServerFnError` 消息，避免泄露 SQL 细节。

use dioxus::prelude::ServerFnError;

/// 应用层统一错误类型。
#[derive(Debug)]
pub enum AppError {
    /// 未认证（401）。
    Unauthorized(&'static str),
    /// 无权限（403）。
    Forbidden(&'static str),
    /// 资源不存在（404）。
    NotFound(&'static str),
    /// 客户端请求错误（400）——业务规则拒绝，消息原样透传给用户。
    BadRequest(String),
    /// 数据库连接失败。
    DbConn(String),
    /// SQL 查询执行失败。
    Query(String),
    /// 数据库事务失败。
    Transaction(String),
    /// 内部通用错误。
    Internal(&'static str),
}

#[cfg(feature = "server")]
impl AppError {
    /// 记录并包装数据库连接错误。
    ///
    /// 对外只暴露通用提示（脱敏），但服务端日志会展开 source 链，记录完整的
    /// 失败原因（如 `db error: connection refused`、IO 错误等）。传入的错误类型
    /// 必须实现 `std::error::Error` —— DB 相关错误（`tokio_postgres::Error`、
    /// `deadpool_postgres::PoolError`）均满足，这一点是上次"日志只显示 `db error`"
    /// 问题的根治前提。
    pub fn db_conn(e: impl std::error::Error) -> Self {
        tracing::error!(
            "DB connection failed: {}",
            crate::db::format_with_sources(&e)
        );
        AppError::DbConn("connection error".to_string())
    }

    /// 记录并包装 SQL 查询错误。
    pub fn query(e: impl std::error::Error) -> Self {
        tracing::error!("Query failed: {}", crate::db::format_with_sources(&e));
        AppError::Query("query error".to_string())
    }

    /// 记录并包装数据库事务错误。
    pub fn tx(e: impl std::error::Error) -> Self {
        tracing::error!("Transaction failed: {}", crate::db::format_with_sources(&e));
        AppError::Transaction("transaction error".to_string())
    }
}

/// 转换为 `ServerFnError`，对数据库类错误返回通用中文提示。
impl From<AppError> for ServerFnError {
    fn from(err: AppError) -> ServerFnError {
        let msg = match &err {
            AppError::Unauthorized(m) => m.to_string(),
            AppError::Forbidden(m) => m.to_string(),
            AppError::NotFound(m) => m.to_string(),
            // BadRequest 是业务规则拒绝（如 SQL 护栏拦截），消息原样透传给用户。
            AppError::BadRequest(m) => m.to_string(),
            AppError::DbConn(_) => "服务暂时不可用".to_string(),
            AppError::Query(_) => "操作失败".to_string(),
            AppError::Transaction(_) => "操作失败".to_string(),
            AppError::Internal(m) => m.to_string(),
        };
        ServerFnError::new(msg)
    }
}

#[cfg(all(test, feature = "server"))]
mod tests {
    use super::*;

    #[test]
    fn unauthorized_message_passthrough() {
        let err: ServerFnError = AppError::Unauthorized("未登录").into();
        let msg = err.to_string();
        assert!(msg.contains("未登录"), "expected '未登录' in: {msg}");
    }

    #[test]
    fn db_conn_hides_internal_details() {
        // 入参是实现 Error 的类型；构造函数内部会把 source 链写进日志，
        // 但对外（ServerFnError）必须只暴露通用提示。
        let src = std::io::Error::other("connection refused on port 5432");
        let err: ServerFnError = AppError::db_conn(src).into();
        let msg = err.to_string();
        assert!(
            !msg.contains("5432"),
            "should not leak internal details: {msg}"
        );
        assert!(
            msg.contains("服务暂时不可用"),
            "expected generic message: {msg}"
        );
    }

    #[test]
    fn query_hides_sql_details() {
        let src = std::io::Error::other("syntax error at SELECT * FROM");
        let err: ServerFnError = AppError::query(src).into();
        let msg = err.to_string();
        assert!(!msg.contains("SELECT"), "should not leak SQL: {msg}");
    }

    #[test]
    fn forbidden_message_passthrough() {
        let err: ServerFnError = AppError::Forbidden("权限不足").into();
        let msg = err.to_string();
        assert!(msg.contains("权限不足"), "expected '权限不足': {msg}");
    }

    #[test]
    fn not_found_message_passthrough() {
        let err: ServerFnError = AppError::NotFound("文章不存在").into();
        let msg = err.to_string();
        assert!(msg.contains("文章不存在"), "expected passthrough: {msg}");
    }

    #[test]
    fn internal_message_passthrough() {
        // Internal 错误的消息原样透传，便于向用户展示可读的内部错误描述。
        let err: ServerFnError = AppError::Internal("内部错误").into();
        let msg = err.to_string();
        assert!(msg.contains("内部错误"), "expected passthrough: {msg}");
    }

    #[test]
    fn transaction_hides_sql_details() {
        // 事务错误同样返回通用提示，不泄露 SQL 细节。
        let src = std::io::Error::other("deadlock detected on UPDATE posts");
        let err: ServerFnError = AppError::tx(src).into();
        let msg = err.to_string();
        assert!(!msg.contains("UPDATE"), "should not leak SQL: {msg}");
        assert!(
            !msg.contains("deadlock"),
            "should not leak error detail: {msg}"
        );
        assert!(msg.contains("操作失败"), "expected generic message: {msg}");
    }

    #[test]
    fn db_conn_query_transaction_all_return_generic_message() {
        // 三类数据库错误对外均返回固定中文提示，避免泄露实现细节。
        let db_conn: ServerFnError = AppError::DbConn("x".into()).into();
        let query: ServerFnError = AppError::Query("x".into()).into();
        let tx: ServerFnError = AppError::Transaction("x".into()).into();

        assert!(db_conn.to_string().contains("服务暂时不可用"));
        assert!(query.to_string().contains("操作失败"));
        assert!(tx.to_string().contains("操作失败"));
    }
}
