#[cfg(test)]
mod tests {
    #![allow(clippy::uninlined_format_args)]

    use scanclient::{HttpConfig, HttpConfigError};

    #[test]
    fn defaults_match_contract() {
        let config = HttpConfig::default();
        let default_profile = scanclient::tls_profiles::rotate(0);

        assert_eq!(config.timeout_secs, 10);
        assert_eq!(config.max_body_size, 10 * 1024 * 1024);
        assert_eq!(config.max_retries, 3);
        assert_eq!(config.retry_delay_ms, 1_000);
        assert_eq!(config.max_redirects, 5);
        assert_eq!(config.proxy, None);
        assert_eq!(config.proxy_user, None);
        assert_eq!(config.proxy_password, None);
        assert_eq!(config.user_agent, default_profile.user_agent);
        assert!(
            config.user_agent.contains("Chrome/"),
            "default User-Agent should be a browser profile, not a scanner banner"
        );
        assert!(config.custom_headers.is_empty());
        assert_eq!(config.rate_limit_per_sec, None);
        assert!(config.tls_verify);
        assert_eq!(config.connect_timeout_secs, 5);
    }

    #[test]
    fn parses_partial_toml_with_defaults() {
        let raw = r#"
timeout_secs = 30
max_body_size = 4096
max_retries = 5
proxy = "http://127.0.0.1:8080"
proxy_user = "alice"
proxy_password = "secret"
rate_limit_per_sec = 8
tls_verify = false

[custom_headers]
X-Scanner = "karyx"
"#;

        let config: HttpConfig = toml::from_str(raw).expect("config should parse");

        assert_eq!(config.timeout_secs, 30);
        assert_eq!(config.max_body_size, 4096);
        assert_eq!(config.max_retries, 5);
        assert_eq!(config.retry_delay_ms, 1_000);
        assert_eq!(config.max_redirects, 5);
        assert_eq!(config.proxy.as_deref(), Some("http://127.0.0.1:8080"));
        assert_eq!(config.proxy_user.as_deref(), Some("alice"));
        assert_eq!(config.proxy_password.as_deref(), Some("secret"));
        assert_eq!(config.rate_limit_per_sec, Some(8));
        assert!(!config.tls_verify);
        assert_eq!(
            config.custom_headers.get("X-Scanner").map(String::as_str),
            Some("karyx")
        );
    }

    #[test]
    fn from_toml_convenience() {
        let config = HttpConfig::from_toml("timeout_secs = 42").unwrap();
        assert_eq!(config.timeout_secs, 42);
        assert_eq!(config.max_retries, 3); // default preserved
    }

    #[test]
    fn to_toml_round_trip() {
        let original = HttpConfig::default();
        let toml_str = original.to_toml().unwrap();
        let parsed = HttpConfig::from_toml(&toml_str).unwrap();
        assert_eq!(original, parsed);
    }

    #[test]
    fn empty_toml_uses_all_defaults() {
        let config = HttpConfig::from_toml("").unwrap();
        assert_eq!(config, HttpConfig::default());
    }

    #[test]
    fn invalid_toml_returns_error() {
        assert!(HttpConfig::from_toml("{{invalid").is_err());
    }

    #[test]
    fn load_file() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("config.toml");
        std::fs::write(&path, "timeout_secs = 99\n").unwrap();
        let config = HttpConfig::load(&path).unwrap();
        assert_eq!(config.timeout_secs, 99);
    }

    #[test]
    fn load_missing_file_errors() {
        let result = HttpConfig::load(std::path::Path::new("/nonexistent/config.toml"));
        assert!(result.is_err());
    }

    #[test]
    fn test_builder_pattern() {
        let mut headers = std::collections::HashMap::new();
        headers.insert("X-Test".to_string(), "Builder".to_string());

        let config = HttpConfig::builder()
            .timeout_secs(45)
            .max_body_size(2048)
            .max_retries(10)
            .retry_delay_ms(500)
            .max_redirects(2)
            .proxy("http://localhost:8888")
            .proxy_user("operator")
            .proxy_password("secret")
            .user_agent("CustomAgent")
            .custom_headers(headers.clone())
            .rate_limit_per_sec(Some(100))
            .tls_verify(false)
            .connect_timeout_secs(15)
            .build();

        assert_eq!(config.timeout_secs, 45);
        assert_eq!(config.max_body_size, 2048);
        assert_eq!(config.max_retries, 10);
        assert_eq!(config.retry_delay_ms, 500);
        assert_eq!(config.max_redirects, 2);
        assert_eq!(config.proxy.as_deref(), Some("http://localhost:8888"));
        assert_eq!(config.proxy_user.as_deref(), Some("operator"));
        assert_eq!(config.proxy_password.as_deref(), Some("secret"));
        assert_eq!(config.user_agent, "CustomAgent");
        assert_eq!(config.custom_headers, headers);
        assert_eq!(config.rate_limit_per_sec, Some(100));
        assert!(!config.tls_verify);
        assert_eq!(config.connect_timeout_secs, 15);
    }

    #[test]
    fn test_error_display() {
        let io_err = HttpConfigError::Io {
            path: std::path::PathBuf::from("/fake/path.toml"),
            source: std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"),
        };
        let io_str = format!("{}", io_err);
        assert!(io_str.contains("/fake/path.toml"));
        assert!(io_str.contains("file not found"));
        assert!(io_str.contains("Fix:"));

        let parse_err = HttpConfig::from_toml("invalid = \n").unwrap_err();
        let parse_str = format!("{}", parse_err);
        assert!(parse_str.contains("invalid HTTP config TOML:"));
        assert!(parse_str.contains("Fix: keep settings at the top level"));
    }

    #[test]
    fn load_with_empty_file_uses_defaults() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("empty.toml");
        std::fs::write(&path, "").unwrap();
        let config = HttpConfig::load(&path).unwrap();
        assert_eq!(config, HttpConfig::default());
    }

    #[test]
    fn load_with_unicode_path_and_unicode_values() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("設定.toml");
        let raw = r#"
user_agent = "scanner/日本語"
[custom_headers]
X-World = "Привет世界"
"#;
        std::fs::write(&path, raw).unwrap();
        let config = HttpConfig::load(&path).unwrap();
        assert_eq!(config.user_agent, "scanner/日本語");
        assert_eq!(
            config.custom_headers.get("X-World").map(String::as_str),
            Some("Привет世界")
        );
    }

    #[test]
    fn from_toml_rejects_null_byte() {
        let result = HttpConfig::from_toml("user_agent = \"a\0b\"");
        assert!(result.is_err());
    }

    #[test]
    fn from_toml_with_huge_input_fails_gracefully() {
        use std::fmt::Write as _;

        let mut huge = String::from("timeout_secs = 1\n");
        huge.push_str("[custom_headers]\n");
        for i in 0..5_000 {
            writeln!(&mut huge, "X{i} = \"{}\"", "A".repeat(50)).unwrap();
        }
        let result = HttpConfig::from_toml(&huge);
        assert!(result.is_ok());
        let parsed = result.unwrap();
        assert_eq!(parsed.timeout_secs, 1);
        assert!(parsed.custom_headers.len() >= 5_000);
    }

    #[test]
    fn to_toml_preserves_unicode_and_empty_values() {
        let mut config = HttpConfig {
            user_agent: "agent/🌐".to_string(),
            ..HttpConfig::default()
        };
        config
            .custom_headers
            .insert("X-Empty".to_string(), String::new());
        config
            .custom_headers
            .insert("X-Emoji".to_string(), "🎉".to_string());
        let toml = config.to_toml().unwrap();
        let parsed = HttpConfig::from_toml(&toml).unwrap();
        assert_eq!(parsed.user_agent, "agent/🌐");
        assert_eq!(
            parsed.custom_headers.get("X-Empty").map(String::as_str),
            Some("")
        );
        assert_eq!(
            parsed.custom_headers.get("X-Emoji").map(String::as_str),
            Some("🎉")
        );
    }

    #[test]
    fn builder_supports_empty_proxy_when_set_explicitly() {
        let config = HttpConfig::builder().proxy("").build();
        assert_eq!(config.proxy.as_deref(), Some(""));
    }

    #[test]
    fn builder_sets_retry_non_idempotent_methods_flag() {
        let config = HttpConfig::builder()
            .retry_non_idempotent_methods(true)
            .build();
        assert!(config.retry_non_idempotent_methods);
    }

    #[test]
    fn builder_sets_tls_exception_flags() {
        let config = HttpConfig::builder()
            .tls_verify(false)
            .tls_accept_invalid_certs(true)
            .tls_accept_invalid_hostnames(true)
            .build();
        assert!(!config.tls_verify);
        assert!(config.tls_accept_invalid_certs);
        assert!(config.tls_accept_invalid_hostnames);
    }

    #[test]
    fn builder_sets_rate_limit_zero_value() {
        let config = HttpConfig::builder().rate_limit_per_sec(Some(0)).build();
        assert_eq!(config.rate_limit_per_sec, Some(0));
    }

    #[test]
    fn from_toml_with_empty_header_key_round_trips() {
        let raw = r#"
[custom_headers]
"" = "bad"
"#;
        let result = HttpConfig::from_toml(raw);
        assert!(result.is_ok());
        let parsed = result.unwrap();
        assert_eq!(
            parsed.custom_headers.get("").map(String::as_str),
            Some("bad")
        );
    }

    // ── ScanClient::from_config no-panic guard ────────────────────────────────

    /// §15 regression: `ScanClient::from_config` previously called
    /// `unimplemented!()` and panicked on every call. This test pins the
    /// fix: from_config must return Ok or a typed Error, never panic.
    ///
    /// The test must run inside a Tokio runtime because `TokioResolver` requires
    /// one at construction time.
    #[tokio::test]
    async fn from_config_does_not_panic() {
        use scanclient::client::ScanClient;
        // from_config builds an ephemeral system resolver.  On most CI
        // environments this succeeds; on a host with no /etc/resolv.conf it
        // may return Err(ResolverInit).  Either outcome is acceptable - what
        // is NOT acceptable is a panic.
        let result = ScanClient::from_config(HttpConfig::default());
        // Ensure it did not panic AND produced a meaningful result.
        match result {
            Ok(_) => { /* resolver initialised fine */ }
            Err(e) => {
                // Must be a typed error, not an unimplemented! panic.
                let msg = e.to_string();
                assert!(
                    !msg.contains("not implemented"),
                    "from_config must return a typed error, not an unimplemented!() message; got: {msg}"
                );
            }
        }
    }
}
