//! Log module tests.
//!
//! ⚠️ Tests that use the global logger (file output, macros) must run serially:
//! `cargo test -p mad-hatter --test test_log -- --test-threads=1`
//! Parallel execution causes race conditions on the shared static LOGGER.

use mad_hatter::log::{self, LogParam, LogLevel, LogConfig, LogOutput, LogFormat, RotationConfig};
use mad_hatter::Sealed;

// ===== LogParam trait dispatch tests =====

#[test]
fn test_log_param_string() {
    let s = String::from("hello");
    let d = LogParam::as_log_display(&s);
    assert_eq!(format!("{}", d), "hello");
}

#[test]
fn test_log_param_str() {
    let s = "world";
    let d = LogParam::as_log_display(&s);
    assert_eq!(format!("{}", d), "world");
}

#[test]
fn test_log_param_number() {
    let n = 42i32;
    let d = LogParam::as_log_display(&n);
    assert_eq!(format!("{}", d), "42");
}

#[test]
fn test_log_param_sealed_string() {
    let sealed = Sealed::new(String::from("secret-value"));
    let d = LogParam::as_log_display(&sealed);
    assert_eq!(format!("{}", d), "secret-value");
}

#[test]
fn test_log_param_sealed_static_str() {
    let sealed = Sealed::new("static-secret");
    let d = LogParam::as_log_display(&sealed);
    assert_eq!(format!("{}", d), "static-secret");
}

#[test]
fn test_log_param_sealed_ref() {
    let sealed = Sealed::new(String::from("ref-secret"));
    let r = &sealed;
    let d = LogParam::as_log_display(&r);
    assert_eq!(format!("{}", d), "ref-secret");
}

// ===== extract_tag tests =====

#[test]
fn test_extract_tag_with_tag() {
    let (tag, msg) = log::extract_tag("[AUTH] login failed");
    assert_eq!(tag, Some("AUTH"));
    assert_eq!(msg, "login failed");
}

#[test]
fn test_extract_tag_without_tag() {
    let (tag, msg) = log::extract_tag("no tag here");
    assert_eq!(tag, None);
    assert_eq!(msg, "no tag here");
}

#[test]
fn test_extract_tag_complex() {
    let (tag, msg) = log::extract_tag("[ORDER-a1b2] payment: status=ok");
    assert_eq!(tag, Some("ORDER-a1b2"));
    assert_eq!(msg, "payment: status=ok");
}

#[test]
fn test_extract_tag_unclosed_bracket() {
    let (tag, msg) = log::extract_tag("[BROKEN message");
    // find(']') returns None → no tag
    assert_eq!(tag, None);
    assert_eq!(msg, "[BROKEN message");
}

// ===== Format tests =====

#[test]
fn test_format_pretty_contains_level_and_message() {
    let line = log::format_pretty(LogLevel::Warn, "[AUTH] test message");
    assert!(line.contains("WARN "));
    assert!(line.contains("[AUTH] test message"));
    assert!(line.ends_with('\n'));
    // Should contain a timestamp-like pattern
    assert!(line.contains('-')); // date separators
    assert!(line.contains(':')); // time separators
}

#[test]
fn test_format_json_with_tag() {
    let line = log::format_json(LogLevel::Error, "[DB] connection lost");
    let v: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
    assert_eq!(v["level"], "ERROR");
    assert_eq!(v["tag"], "DB");
    assert_eq!(v["msg"], "connection lost");
    assert!(v["ts"].is_string());
}

#[test]
fn test_format_json_without_tag() {
    let line = log::format_json(LogLevel::Info, "simple message");
    let v: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
    assert_eq!(v["level"], "INFO");
    assert!(v.get("tag").is_none());
    assert_eq!(v["msg"], "simple message");
}

#[test]
fn test_format_json_escapes_special_chars() {
    let line = log::format_json(LogLevel::Warn, "[TEST] has \"quotes\" and\nnewline");
    let v: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
    assert_eq!(v["msg"], "has \"quotes\" and\nnewline");
}

// ===== File output tests =====

#[test]
fn test_file_output_pretty() {
    let dir = tempfile::tempdir().unwrap();
    let log_path = dir.path().join("test.log");
    let log_path_str = log_path.to_str().unwrap().to_string();

    log::init(LogConfig {
        level: LogLevel::Debug,
        output: LogOutput::File(log_path_str),
        format: LogFormat::Pretty,
        rotation: None,
    });

    log::__log_write(LogLevel::Warn, format_args!("[TEST] file output works"));
    log::flush();

    let content = std::fs::read_to_string(&log_path).unwrap();
    assert!(content.contains("WARN "));
    assert!(content.contains("[TEST] file output works"));
}

#[test]
fn test_file_output_json() {
    let dir = tempfile::tempdir().unwrap();
    let log_path = dir.path().join("test.log");
    let log_path_str = log_path.to_str().unwrap().to_string();

    log::init(LogConfig {
        level: LogLevel::Debug,
        output: LogOutput::File(log_path_str),
        format: LogFormat::Json,
        rotation: None,
    });

    log::__log_write(LogLevel::Info, format_args!("[HUB] json test"));
    log::flush();

    let content = std::fs::read_to_string(&log_path).unwrap();
    let v: serde_json::Value = serde_json::from_str(content.trim()).unwrap();
    assert_eq!(v["level"], "INFO");
    assert_eq!(v["tag"], "HUB");
    assert_eq!(v["msg"], "json test");
}

// ===== Level filter tests =====

#[test]
fn test_level_filter() {
    let dir = tempfile::tempdir().unwrap();
    let log_path = dir.path().join("test.log");
    let log_path_str = log_path.to_str().unwrap().to_string();

    log::init(LogConfig {
        level: LogLevel::Warn,
        output: LogOutput::File(log_path_str.clone()),
        format: LogFormat::Pretty,
        rotation: None,
    });

    // These should be written (Error ≤ Warn, Warn ≤ Warn)
    log::__log_write(LogLevel::Error, format_args!("error msg"));
    log::__log_write(LogLevel::Warn, format_args!("warn msg"));
    // These should be filtered out (Info > Warn, Debug > Warn)
    log::__log_write(LogLevel::Info, format_args!("info msg"));
    log::__log_write(LogLevel::Debug, format_args!("debug msg"));
    log::flush();

    let content = std::fs::read_to_string(&log_path).unwrap();
    assert!(content.contains("error msg"));
    assert!(content.contains("warn msg"));
    assert!(!content.contains("info msg"));
    assert!(!content.contains("debug msg"));
}

// ===== Runtime level change =====

#[test]
fn test_set_level() {
    log::set_level(LogLevel::Error);
    assert_eq!(log::current_level(), LogLevel::Error);
    log::set_level(LogLevel::Trace);
    assert_eq!(log::current_level(), LogLevel::Trace);
    // Restore default
    log::set_level(LogLevel::Info);
}

// ===== Rotation test =====

#[test]
fn test_file_rotation() {
    let dir = tempfile::tempdir().unwrap();
    let log_path = dir.path().join("app.log");
    let log_path_str = log_path.to_str().unwrap().to_string();

    // Use tiny max_size to trigger rotation quickly
    // 1 MB is too large for a test, so we test the rotation logic
    // by directly using FileWriter internals... but those are private.
    // Instead, we test with max_size_mb=0 which means rotate on every write.
    // Wait, 0 * 1024 * 1024 = 0 bytes, so every write triggers rotation.

    log::init(LogConfig {
        level: LogLevel::Debug,
        output: LogOutput::File(log_path_str.clone()),
        format: LogFormat::Pretty,
        rotation: Some(RotationConfig {
            max_size_mb: 0, // 0 bytes = rotate on every write
            max_files: 3,
        }),
    });

    // Write several messages — each should trigger rotation
    for i in 0..5 {
        log::__log_write(LogLevel::Info, format_args!("rotation test {}", i));
    }
    log::flush();

    // Current file should exist
    assert!(log_path.exists(), "current log file should exist");

    // At least .1 should exist (rotated)
    let rotated_1 = dir.path().join("app.log.1");
    assert!(rotated_1.exists(), "app.log.1 should exist");

    // .4 and .5 should NOT exist (max_files=3, so only .1, .2, .3 kept)
    let rotated_4 = dir.path().join("app.log.4");
    assert!(!rotated_4.exists(), "app.log.4 should not exist (max_files=3)");
}

// ===== Macro integration tests =====
// These test that the macros compile and produce correct output.
// Since macros use the global logger, they share state with file tests above.
// We reinitialize the logger for each test.

#[test]
fn test_macro_basic() {
    let dir = tempfile::tempdir().unwrap();
    let log_path = dir.path().join("macro.log");
    let log_path_str = log_path.to_str().unwrap().to_string();

    log::init(LogConfig {
        level: LogLevel::Debug,
        output: LogOutput::File(log_path_str),
        format: LogFormat::Pretty,
        rotation: None,
    });

    mad_hatter::__mad_warn!("[TEST] macro works: x={}", 42);
    log::flush();

    let content = std::fs::read_to_string(&log_path).unwrap();
    assert!(content.contains("WARN "));
    assert!(content.contains("[TEST] macro works: x=42"));
}

#[test]
fn test_macro_with_sealed() {
    let dir = tempfile::tempdir().unwrap();
    let log_path = dir.path().join("sealed.log");
    let log_path_str = log_path.to_str().unwrap().to_string();

    log::init(LogConfig {
        level: LogLevel::Debug,
        output: LogOutput::File(log_path_str),
        format: LogFormat::Pretty,
        rotation: None,
    });

    let sealed_msg = Sealed::new(String::from("unsealed-in-log"));
    mad_hatter::__mad_info!("[AUTH] msg={}", sealed_msg);
    log::flush();

    let content = std::fs::read_to_string(&log_path).unwrap();
    assert!(content.contains("INFO "));
    assert!(content.contains("[AUTH] msg=unsealed-in-log"));
}

#[test]
fn test_macro_mixed_params() {
    let dir = tempfile::tempdir().unwrap();
    let log_path = dir.path().join("mixed.log");
    let log_path_str = log_path.to_str().unwrap().to_string();

    log::init(LogConfig {
        level: LogLevel::Debug,
        output: LogOutput::File(log_path_str),
        format: LogFormat::Pretty,
        rotation: None,
    });

    let user_id = "user-123";
    let sealed_err = Sealed::new(String::from("access denied"));
    mad_hatter::__mad_error!("[API] user={}, error={}", user_id, sealed_err);
    log::flush();

    let content = std::fs::read_to_string(&log_path).unwrap();
    assert!(content.contains("ERROR"));
    assert!(content.contains("[API] user=user-123, error=access denied"));
}

#[test]
fn test_macro_no_args() {
    let dir = tempfile::tempdir().unwrap();
    let log_path = dir.path().join("noargs.log");
    let log_path_str = log_path.to_str().unwrap().to_string();

    log::init(LogConfig {
        level: LogLevel::Debug,
        output: LogOutput::File(log_path_str),
        format: LogFormat::Pretty,
        rotation: None,
    });

    mad_hatter::__mad_debug!("[BOOT] system started");
    log::flush();

    let content = std::fs::read_to_string(&log_path).unwrap();
    assert!(content.contains("DEBUG"));
    assert!(content.contains("[BOOT] system started"));
}

// ===== Macro re-export via log module =====

#[test]
fn test_macro_via_log_module() {
    let dir = tempfile::tempdir().unwrap();
    let log_path = dir.path().join("reexport.log");
    let log_path_str = log_path.to_str().unwrap().to_string();

    log::init(LogConfig {
        level: LogLevel::Trace,
        output: LogOutput::File(log_path_str),
        format: LogFormat::Pretty,
        rotation: None,
    });

    // Use the re-exported macros through log module
    log::error!("[E] error msg");
    log::warn!("[W] warn msg");
    log::info!("[I] info msg");
    log::debug!("[D] debug msg");
    log::trace!("[T] trace msg");
    log::flush();

    let content = std::fs::read_to_string(&log_path).unwrap();
    assert!(content.contains("[E] error msg"));
    assert!(content.contains("[W] warn msg"));
    assert!(content.contains("[I] info msg"));
    assert!(content.contains("[D] debug msg"));
    assert!(content.contains("[T] trace msg"));
}

// ===== log_config! time_formats tests =====

mod log_config_time_formats {
    use chrono::NaiveDateTime;

    mad_hatter::log_config! {
        consts {
            TEST_TIME_FMT: &str = "%m/%d %H:%M:%S";
            TEST_DATE_FMT: &str = "%Y-%m-%d";
        }
        formats {
            fn test_crash_line(ts: &str, msg: &str) = "[{}] {}";
        }
        time_formats {
            fn format_test_time(time: &NaiveDateTime) = TEST_TIME_FMT;
            fn format_test_date(time: &NaiveDateTime) = TEST_DATE_FMT;
        }
    }

    #[test]
    fn test_time_format_basic() {
        let dt = NaiveDateTime::parse_from_str("20260326120000", "%Y%m%d%H%M%S").unwrap();
        let result = format_test_time(&dt);
        assert_eq!(result, "03/26 12:00:00");
    }

    #[test]
    fn test_date_format_basic() {
        let dt = NaiveDateTime::parse_from_str("20260326120000", "%Y%m%d%H%M%S").unwrap();
        let result = format_test_date(&dt);
        assert_eq!(result, "2026-03-26");
    }

    #[test]
    fn test_consts_and_formats_still_work() {
        assert_eq!(TEST_TIME_FMT, "%m/%d %H:%M:%S");
        assert_eq!(test_crash_line("12:00", "boom"), "[12:00] boom");
    }
}

// ===== log_config! backward compat (two blocks only) =====

mod log_config_two_blocks {
    mad_hatter::log_config! {
        consts {
            OLD_LOG_FILE: &str = "old.log";
        }
        formats {
            fn old_format(x: &str) = "old: {}";
        }
    }

    #[test]
    fn test_two_block_compat() {
        assert_eq!(OLD_LOG_FILE, "old.log");
        assert_eq!(old_format("val"), "old: val");
    }
}