inklog 0.3.0-rc.1

Enterprise-grade Rust logging infrastructure
// Copyright (c) 2026 Kirky.X
// SPDX-License-Identifier: MIT
//! 日志级别枚举定义
//!
//! 提供类型安全的日志级别表示,消除硬编码字符串字面量

use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;

/// 日志级别枚举
///
/// 用于表示日志记录的严重程度,按从低到高的顺序排列
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[repr(u8)]
pub enum LogLevel {
    /// 跟踪级别 - 最详细的日志
    Trace = 0,
    /// 调试级别 - 开发时使用的详细信息
    Debug = 1,
    /// 信息级别 - 一般信息
    #[default]
    Info = 2,
    /// 警告级别 - 警告信息
    Warn = 3,
    /// 错误级别 - 错误信息
    Error = 4,
    /// 致命级别 - 导致程序终止的错误
    Fatal = 5,
}

impl LogLevel {
    /// 从字符串解析日志级别(大小写不敏感)
    ///
    /// Uses case-insensitive ASCII comparison to avoid heap allocation
    /// from `to_uppercase()`.
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(s: &str) -> Option<Self> {
        // Case-insensitive ASCII matching without allocation
        if s.eq_ignore_ascii_case("trace") {
            Some(LogLevel::Trace)
        } else if s.eq_ignore_ascii_case("debug") {
            Some(LogLevel::Debug)
        } else if s.eq_ignore_ascii_case("info") {
            Some(LogLevel::Info)
        } else if s.eq_ignore_ascii_case("warn") || s.eq_ignore_ascii_case("warning") {
            Some(LogLevel::Warn)
        } else if s.eq_ignore_ascii_case("error") {
            Some(LogLevel::Error)
        } else if s.eq_ignore_ascii_case("fatal") || s.eq_ignore_ascii_case("critical") {
            Some(LogLevel::Fatal)
        } else {
            None
        }
    }

    /// Returns `true` if `s` is a recognised log level name (case-insensitive),
    /// including aliases `warning` (→ Warn) and `critical` (→ Fatal).
    pub fn is_valid_level(s: &str) -> bool {
        Self::from_str(s).is_some()
    }

    /// Canonical list of valid log level names (lowercase, including aliases).
    pub const VALID_LEVEL_STRINGS: &'static [&'static str] = &[
        "trace", "debug", "info", "warn", "warning", "error", "fatal", "critical",
    ];

    /// 获取日志级别的字符串表示
    pub fn as_str(&self) -> &'static str {
        match self {
            LogLevel::Trace => "TRACE",
            LogLevel::Debug => "DEBUG",
            LogLevel::Info => "INFO",
            LogLevel::Warn => "WARN",
            LogLevel::Error => "ERROR",
            LogLevel::Fatal => "FATAL",
        }
    }

    /// 获取日志级别的短字符串表示(用于显示)
    pub fn as_short_str(&self) -> &'static str {
        match self {
            LogLevel::Trace => "TRC",
            LogLevel::Debug => "DBG",
            LogLevel::Info => "INF",
            LogLevel::Warn => "WRN",
            LogLevel::Error => "ERR",
            LogLevel::Fatal => "FTL",
        }
    }

    /// Returns the translated log level name for the current locale.
    ///
    /// Unlike [`as_str()`](Self::as_str) which always returns English uppercase
    /// names (e.g. "INFO"), this method returns the locale-aware translation.
    pub fn localized_name(&self) -> String {
        match self {
            LogLevel::Trace => crate::i18n::tr("log_level-name_trace"),
            LogLevel::Debug => crate::i18n::tr("log_level-name_debug"),
            LogLevel::Info => crate::i18n::tr("log_level-name_info"),
            LogLevel::Warn => crate::i18n::tr("log_level-name_warn"),
            LogLevel::Error => crate::i18n::tr("log_level-name_error"),
            LogLevel::Fatal => crate::i18n::tr("log_level-name_fatal"),
        }
    }
}

impl fmt::Display for LogLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl FromStr for LogLevel {
    type Err = LogLevelParseError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::from_str(s).ok_or_else(|| LogLevelParseError::Unknown(s.to_string()))
    }
}

/// 解析日志级别字符串的错误
#[derive(Debug)]
pub enum LogLevelParseError {
    Unknown(String),
}

impl std::fmt::Display for LogLevelParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LogLevelParseError::Unknown(level) => {
                let mut args = fluent_bundle::FluentArgs::new();
                args.set("level", level);
                write!(
                    f,
                    "{}",
                    crate::i18n::tr_args("config-unknown_log_level", args)
                )
            }
        }
    }
}

impl std::error::Error for LogLevelParseError {}

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

    #[test]
    fn test_from_str_valid() {
        assert_eq!(LogLevel::from_str("INFO"), Some(LogLevel::Info));
        assert_eq!(LogLevel::from_str("info"), Some(LogLevel::Info));
        assert_eq!(LogLevel::from_str("Info"), Some(LogLevel::Info)); // mixed case
        assert_eq!(LogLevel::from_str("WARN"), Some(LogLevel::Warn));
        assert_eq!(LogLevel::from_str("WARNING"), Some(LogLevel::Warn));
        assert_eq!(LogLevel::from_str("warning"), Some(LogLevel::Warn));
        assert_eq!(LogLevel::from_str("ERROR"), Some(LogLevel::Error));
        assert_eq!(LogLevel::from_str("FATAL"), Some(LogLevel::Fatal));
        assert_eq!(LogLevel::from_str("CRITICAL"), Some(LogLevel::Fatal));
        assert_eq!(LogLevel::from_str("TRACE"), Some(LogLevel::Trace));
        assert_eq!(LogLevel::from_str("DEBUG"), Some(LogLevel::Debug));
    }

    #[test]
    fn test_from_str_invalid() {
        assert_eq!(LogLevel::from_str("INVALID"), None);
        assert_eq!(LogLevel::from_str(""), None);
    }

    #[test]
    fn test_as_str() {
        assert_eq!(LogLevel::Info.as_str(), "INFO");
        assert_eq!(LogLevel::Error.as_str(), "ERROR");
    }

    #[test]
    fn test_ordering() {
        assert!(LogLevel::Error > LogLevel::Debug);
        assert!(LogLevel::Info <= LogLevel::Warn);
        assert!(LogLevel::Trace < LogLevel::Fatal);
    }

    #[test]
    fn test_as_str_all_variants() {
        assert_eq!(LogLevel::Trace.as_str(), "TRACE");
        assert_eq!(LogLevel::Debug.as_str(), "DEBUG");
        assert_eq!(LogLevel::Info.as_str(), "INFO");
        assert_eq!(LogLevel::Warn.as_str(), "WARN");
        assert_eq!(LogLevel::Error.as_str(), "ERROR");
        assert_eq!(LogLevel::Fatal.as_str(), "FATAL");
    }

    #[test]
    fn test_as_short_str_all_variants() {
        assert_eq!(LogLevel::Trace.as_short_str(), "TRC");
        assert_eq!(LogLevel::Debug.as_short_str(), "DBG");
        assert_eq!(LogLevel::Info.as_short_str(), "INF");
        assert_eq!(LogLevel::Warn.as_short_str(), "WRN");
        assert_eq!(LogLevel::Error.as_short_str(), "ERR");
        assert_eq!(LogLevel::Fatal.as_short_str(), "FTL");
    }

    #[test]
    fn test_display_matches_as_str() {
        for level in [
            LogLevel::Trace,
            LogLevel::Debug,
            LogLevel::Info,
            LogLevel::Warn,
            LogLevel::Error,
            LogLevel::Fatal,
        ] {
            assert_eq!(format!("{}", level), level.as_str());
        }
    }

    #[test]
    fn test_from_str_trait_valid() {
        let level: LogLevel = "debug".parse().expect("valid level should parse");
        assert_eq!(level, LogLevel::Debug);

        let level: LogLevel = "WARNING".parse().expect("WARNING should parse to Warn");
        assert_eq!(level, LogLevel::Warn);

        let level: LogLevel = "CRITICAL".parse().expect("CRITICAL should parse to Fatal");
        assert_eq!(level, LogLevel::Fatal);
    }

    #[test]
    fn test_from_str_trait_invalid_returns_error() {
        let result: Result<LogLevel, _> = "invalid_level".parse();
        let err = result.expect_err("invalid level should error");
        assert!(format!("{}", err).contains("Unknown log level"));
        assert!(format!("{}", err).contains("invalid_level"));
    }

    #[test]
    fn test_from_str_trait_empty_string() {
        let result: Result<LogLevel, _> = "".parse();
        assert!(result.is_err());
    }

    // ── localized_name() tests ────────────────────────────────────

    #[test]
    fn test_localized_name_all_levels() {
        let levels = [
            (LogLevel::Trace, "TRACE"),
            (LogLevel::Debug, "DEBUG"),
            (LogLevel::Info, "INFO"),
            (LogLevel::Warn, "WARN"),
            (LogLevel::Error, "ERROR"),
            (LogLevel::Fatal, "FATAL"),
        ];
        for (level, expected_en) in levels {
            let name = level.localized_name();
            // In en locale (or fallback), should match English name.
            // In other locales, should at least be non-empty.
            assert!(!name.is_empty(), "localized_name() empty for {:?}", level);
            // If current locale is en, verify exact match
            if crate::i18n::current_locale() == "en" {
                assert_eq!(name, expected_en, "en locale mismatch for {:?}", level);
            }
        }
    }

    #[test]
    fn test_localized_name_differs_from_as_str_in_zh_cn() {
        // This test only validates behavior when locale is zh-CN.
        // In other locales, it simply confirms localized_name() is non-empty.
        let name = LogLevel::Info.localized_name();
        assert!(!name.is_empty());
        if crate::i18n::current_locale() == "zh-CN" {
            assert_eq!(name, "信息");
            assert_ne!(name, LogLevel::Info.as_str());
        }
    }
}