#[cfg(test)]
mod tests {
    #![allow(
        clippy::manual_let_else,
        clippy::match_same_arms,
        clippy::uninlined_format_args,
        clippy::unreadable_literal
    )]

    use std::{
        collections::HashMap,
        net::SocketAddr,
        sync::{
            Arc,
            atomic::{AtomicUsize, Ordering},
        },
        time::Duration,
    };

    use reqwest::{Method, StatusCode};
    use tokio::{
        io::{AsyncReadExt, AsyncWriteExt},
        net::TcpListener,
        sync::Mutex,
    };

    use hickory_resolver::TokioResolver;
    use scanclient::{Error, HttpConfig, Result, ScanClient};

    fn build_client(config: HttpConfig) -> Result<ScanClient> {
        let resolver = Arc::new(
            TokioResolver::builder_tokio()
                .expect("tokio async resolver")
                .build(),
        );
        ScanClient::new(config, resolver)
    }
    use scanclient::retry::{MAX_RETRY_DELAY_MS, backoff_delay};

    #[test]
    fn client_builds_from_defaults() {
        let client = build_client(HttpConfig::default()).expect("client should build");
        assert_eq!(client.config.max_body_size, 10 * 1024 * 1024);
        assert_eq!(client.config.max_retries, 3);
    }

    #[tokio::test]
    async fn retries_until_success() {
        let attempts = Arc::new(AtomicUsize::new(0));
        let server_attempts = attempts.clone();
        let (address, server) = spawn_test_server(move |_| {
            let current = server_attempts.fetch_add(1, Ordering::SeqCst) + 1;
            async move {
                if current < 3 {
                    http_response(500, &[("content-length", "5")], b"error")
                } else {
                    http_response(200, &[("content-length", "2")], b"ok")
                }
            }
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig {
            max_retries: 4,
            retry_delay_ms: 10,
            ..HttpConfig::default()
        })
        .expect("client should build");

        let response = client
            .get(&format!("http://{address}/health"))
            .await
            .expect("request should eventually succeed");

        assert_eq!(response.status(), reqwest::StatusCode::OK);
        assert_eq!(response.body_text().expect("utf8 body"), "ok");
        assert_eq!(attempts.load(Ordering::SeqCst), 3);
    }

    #[tokio::test]
    async fn retries_429_with_retry_after_until_success() {
        let attempts = Arc::new(AtomicUsize::new(0));
        let server_attempts = attempts.clone();
        let (address, server) = spawn_test_server(move |_| {
            let current = server_attempts.fetch_add(1, Ordering::SeqCst) + 1;
            async move {
                if current < 3 {
                    http_response(
                        429,
                        &[("retry-after", "0"), ("content-length", "8")],
                        b"slowdown",
                    )
                } else {
                    http_response(200, &[("content-length", "2")], b"ok")
                }
            }
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig {
            max_retries: 4,
            retry_delay_ms: 10,
            ..HttpConfig::default()
        })
        .unwrap();

        let response = client.get(&format!("http://{address}/rate")).await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(attempts.load(Ordering::SeqCst), 3);
    }

    #[tokio::test]
    async fn request_builder_uses_default_headers() {
        let captured = Arc::new(Mutex::new(HashMap::<String, String>::new()));
        let captured_headers = captured.clone();
        let (address, server) = spawn_test_server(move |headers| {
            let captured_headers = captured_headers.clone();
            async move {
                *captured_headers.lock().await = headers;
                http_response(200, &[("content-length", "7")], b"headers")
            }
        })
        .await;
        tokio::spawn(server);

        let mut custom_headers = HashMap::new();
        custom_headers.insert("x-scanner".to_string(), "wafrift".to_string());

        let client = ScanClient::from_config(HttpConfig {
            user_agent: "Mozilla/5.0 test".to_string(),
            custom_headers,
            ..HttpConfig::default()
        })
        .expect("client should build");

        let response = client
            .get(&format!("http://{address}/headers"))
            .await
            .expect("request should succeed");

        assert_eq!(response.status(), reqwest::StatusCode::OK);

        let headers = captured.lock().await;
        assert_eq!(
            headers.get("user-agent").map(String::as_str),
            Some("Mozilla/5.0 test")
        );
        let facts = guise_profiles::default_profile_facts();
        assert_eq!(
            headers.get("accept").map(String::as_str),
            Some(facts.accept)
        );
        assert_eq!(
            headers.get("accept-language").map(String::as_str),
            Some(facts.accept_language)
        );
        assert_eq!(
            headers.get("x-scanner").map(String::as_str),
            Some("wafrift")
        );
    }

    async fn spawn_test_server<F, Fut>(
        handler: F,
    ) -> (SocketAddr, impl std::future::Future<Output = ()>)
    where
        F: Fn(HashMap<String, String>) -> Fut + Send + Sync + 'static,
        Fut: std::future::Future<Output = Vec<u8>> + Send + 'static,
    {
        let listener = TcpListener::bind("127.0.0.1:0")
            .await
            .expect("listener should bind");
        let address = listener
            .local_addr()
            .expect("listener should have an address");
        let handler = Arc::new(handler);

        let server = async move {
            loop {
                let (mut stream, _) = match listener.accept().await {
                    Ok(result) => result,
                    Err(_) => break,
                };
                let handler = handler.clone();

                tokio::spawn(async move {
                    let mut buffer = vec![0_u8; 4096];
                    let read = match stream.read(&mut buffer).await {
                        Ok(read) => read,
                        Err(_) => return,
                    };
                    let request = String::from_utf8_lossy(&buffer[..read]);
                    let headers = parse_headers(&request);
                    let response = handler(headers).await;
                    let _ = stream.write_all(&response).await;
                });
            }
        };

        (address, server)
    }

    fn parse_headers(raw_request: &str) -> HashMap<String, String> {
        raw_request
            .lines()
            .skip(1)
            .take_while(|line| !line.is_empty())
            .filter_map(|line| line.split_once(':'))
            .map(|(name, value)| (name.trim().to_ascii_lowercase(), value.trim().to_string()))
            .collect()
    }

    fn http_response(status: u16, headers: &[(&str, &str)], body: &[u8]) -> Vec<u8> {
        let reason = match status {
            200 => "OK",
            302 => "Found",
            429 => "Too Many Requests",
            500 => "Internal Server Error",
            503 => "Service Unavailable",
            _ => "OK",
        };

        let mut response = format!("HTTP/1.1 {status} {reason}\r\n").into_bytes();
        for (name, value) in headers {
            response.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
        }
        response.extend_from_slice(b"\r\n");
        response.extend_from_slice(body);
        response
    }

    #[test]
    fn test_backoff_delay() {
        assert_eq!(backoff_delay(100, 0), Duration::from_millis(100));
        assert_eq!(backoff_delay(100, 1), Duration::from_millis(200));
        assert_eq!(backoff_delay(100, 2), Duration::from_millis(400));
        assert_eq!(
            backoff_delay(100, 16),
            Duration::from_millis(MAX_RETRY_DELAY_MS)
        );
        assert_eq!(
            backoff_delay(100, 20),
            Duration::from_millis(MAX_RETRY_DELAY_MS)
        );
    }

    #[tokio::test]
    async fn test_rate_limiter_creation() {
        // Valid limit
        let config = HttpConfig {
            rate_limit_per_sec: Some(10),
            ..Default::default()
        };
        let client = ScanClient::from_config(config).unwrap();
        assert!(client.rate_gate.is_some());

        // No limit
        let config_no_limit = HttpConfig {
            rate_limit_per_sec: None,
            ..Default::default()
        };
        let client_no_limit = ScanClient::from_config(config_no_limit).unwrap();
        assert!(client_no_limit.rate_gate.is_none());
    }

    #[tokio::test]
    async fn test_max_retries_exhausted() {
        let attempts = Arc::new(AtomicUsize::new(0));
        let server_attempts = attempts.clone();
        let (address, server) = spawn_test_server(move |_| {
            let _ = server_attempts.fetch_add(1, Ordering::SeqCst);
            async move { http_response(500, &[("content-length", "5")], b"error") }
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig {
            max_retries: 2,
            retry_delay_ms: 5,
            ..HttpConfig::default()
        })
        .unwrap();

        let response = client.get(&format!("http://{address}/health")).await;

        // It should either return the final 500 response or an error, based on execute_raw logic
        // Current logic for 500 status returns the 500 response after all retries.
        let resp = response.unwrap();
        assert_eq!(resp.status(), reqwest::StatusCode::INTERNAL_SERVER_ERROR);

        // 1 initial try + 2 retries = 3 attempts
        assert_eq!(attempts.load(Ordering::SeqCst), 3);
    }

    #[tokio::test]
    async fn rejects_redirects_to_private_ips() {
        let (address, server) = spawn_test_server(move |_| async move {
            http_response(
                302,
                &[
                    ("location", "http://127.0.0.1:8080/private"),
                    ("content-length", "0"),
                ],
                b"",
            )
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig::default()).unwrap();
        let error = client
            .get(&format!("http://{address}/redirect"))
            .await
            .unwrap_err();

        match error {
            Error::RedirectBlocked { url } => {
                assert_eq!(url, "http://127.0.0.1:8080/private");
            }
            other => panic!("expected redirect block, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn redirects_blocked_when_proxy_configured() {
        let (address, server) = spawn_test_server(move |_| async move {
            http_response(
                302,
                &[
                    ("location", "http://127.0.0.1:8080/private"),
                    ("content-length", "0"),
                ],
                b"",
            )
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig {
            proxy: Some(format!("http://{address}")),
            ..HttpConfig::default()
        })
        .unwrap();

        let error = client
            .get(&format!("http://{address}/redirect"))
            .await
            .unwrap_err();

        match error {
            Error::RedirectBlocked { url } => {
                assert_eq!(url, "http://127.0.0.1:8080/private");
            }
            other => panic!("expected redirect block, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn enforces_max_body_size() {
        let (address, server) = spawn_test_server(move |_| async move {
            http_response(200, &[("content-length", "12")], b"hello world!")
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig {
            max_body_size: 5,
            ..HttpConfig::default()
        })
        .unwrap();

        let error = client
            .get(&format!("http://{address}/body"))
            .await
            .unwrap_err();
        match error {
            Error::ResponseBodyTooLarge { limit, observed } => {
                assert_eq!(limit, 5);
                assert_eq!(observed, 12);
            }
            other => panic!("expected body limit error, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn proxy_basic_auth_adds_proxy_authorization_header() {
        let captured = Arc::new(Mutex::new(HashMap::<String, String>::new()));
        let captured_headers = captured.clone();
        let (proxy_address, server) = spawn_test_server(move |headers| {
            let captured_headers = captured_headers.clone();
            async move {
                *captured_headers.lock().await = headers;
                http_response(200, &[("content-length", "2")], b"ok")
            }
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig {
            proxy: Some(format!("http://{proxy_address}")),
            proxy_user: Some(String::from("scanner")),
            proxy_password: Some(String::from("secret")),
            ..HttpConfig::default()
        })
        .unwrap();

        let response = client.get("http://example.com/proxied").await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let headers = captured.lock().await;
        assert_eq!(
            headers.get("proxy-authorization").map(String::as_str),
            Some("Basic c2Nhbm5lcjpzZWNyZXQ=")
        );
    }

    #[test]
    fn test_error_display_from_impls() {
        let err = Error::InvalidProxy("bad-proxy".to_string());
        let display_str = format!("{}", err);
        assert!(display_str.contains("invalid proxy URL `bad-proxy`"));

        let err2 = Error::InvalidHeaderName {
            name: "bad header".to_string(),
        };
        let display_str2 = format!("{}", err2);
        assert!(display_str2.contains("invalid header name `bad header`"));

        let err3 = Error::InvalidHeaderValue {
            name: "test-head".to_string(),
        };
        let display_str3 = format!("{}", err3);
        assert!(display_str3.contains("invalid header value for `test-head`"));

        let _ = Error::UnclonableRequest;
    }

    #[tokio::test]
    async fn test_head_and_post_requests() {
        let (address, server) = spawn_test_server(move |_headers| async move {
            http_response(200, &[("content-length", "2")], b"ok")
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig::default()).unwrap();

        // test HEAD
        let head_resp = client.head(&format!("http://{address}/")).await.unwrap();
        assert_eq!(head_resp.status(), reqwest::StatusCode::OK);

        // test POST
        let post_resp = client
            .post(&format!("http://{address}/"), "body")
            .await
            .unwrap();
        assert_eq!(post_resp.status(), reqwest::StatusCode::OK);
        assert_eq!(post_resp.body_text().unwrap(), "ok");
    }

    #[tokio::test]
    async fn execute_with_custom_request_builder() {
        let (address, server) = spawn_test_server(move |_headers| async move {
            http_response(200, &[("content-length", "6")], b"custom")
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig::default()).unwrap();
        let builder = client.request(Method::GET, &format!("http://{address}/custom"));
        let response = client.execute(builder).await.unwrap();
        assert_eq!(response.status(), reqwest::StatusCode::OK);
        assert_eq!(response.body_text().unwrap(), "custom");
    }

    #[tokio::test]
    async fn execute_empty_url_fails_gracefully() {
        let client = ScanClient::from_config(HttpConfig::default()).unwrap();
        let builder = client.request(Method::GET, "");
        let result = client.execute(builder).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn invalid_header_value_with_null_byte_is_rejected() {
        let mut headers = HashMap::new();
        headers.insert("x-null".to_string(), "bad\0value".to_string());
        let config = HttpConfig {
            custom_headers: headers,
            ..HttpConfig::default()
        };
        let result = ScanClient::from_config(config);
        assert!(matches!(result, Err(Error::InvalidHeaderValue { .. })));
    }

    #[tokio::test]
    async fn concurrent_get_requests_share_client() {
        let hits = Arc::new(AtomicUsize::new(0));
        let hits_server = hits.clone();
        let (address, server) = spawn_test_server(move |_headers| {
            let hits_server = hits_server.clone();
            async move {
                hits_server.fetch_add(1, Ordering::SeqCst);
                http_response(200, &[("content-length", "2")], b"ok")
            }
        })
        .await;
        tokio::spawn(server);

        let client = Arc::new(ScanClient::from_config(HttpConfig::default()).unwrap());
        let mut handles = Vec::new();
        for _ in 0..20 {
            let c = client.clone();
            let url = format!("http://{address}/parallel");
            handles.push(tokio::spawn(
                async move { c.get(&url).await.unwrap().status() },
            ));
        }

        for handle in handles {
            assert_eq!(handle.await.unwrap(), reqwest::StatusCode::OK);
        }
        assert_eq!(hits.load(Ordering::SeqCst), 20);
    }

    #[tokio::test]
    async fn post_huge_body_succeeds() {
        let (address, server) = spawn_test_server(move |_headers| async move {
            http_response(200, &[("content-length", "4")], b"huge")
        })
        .await;
        tokio::spawn(server);

        let payload = "A".repeat(2 * 1024 * 1024);
        let client = ScanClient::from_config(HttpConfig::default()).unwrap();
        let response = client
            .post(&format!("http://{address}/upload"), payload)
            .await
            .unwrap();
        assert_eq!(response.status(), reqwest::StatusCode::OK);
        assert_eq!(response.body_text().unwrap(), "huge");
    }

    #[tokio::test]
    async fn post_binary_body_with_null_and_unicode_succeeds() {
        let (address, server) = spawn_test_server(move |_headers| async move {
            http_response(200, &[("content-length", "2")], b"ok")
        })
        .await;
        tokio::spawn(server);

        let payload = vec![
            0x66, 0x6f, 0x6f, 0x00, 0x62, 0x61, 0x72, 0xf0, 0x9f, 0x8e, 0x89,
        ];
        let client = ScanClient::from_config(HttpConfig::default()).unwrap();
        let response = client
            .post(&format!("http://{address}/binary"), payload)
            .await
            .unwrap();
        assert_eq!(response.status(), reqwest::StatusCode::OK);
    }

    #[tokio::test]
    async fn post_non_idempotent_not_retried_by_default() {
        let attempts = Arc::new(AtomicUsize::new(0));
        let attempts_server = attempts.clone();
        let (address, server) = spawn_test_server(move |_| {
            let attempts_server = attempts_server.clone();
            async move {
                attempts_server.fetch_add(1, Ordering::SeqCst);
                http_response(500, &[("content-length", "5")], b"error")
            }
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig {
            max_retries: 5,
            retry_delay_ms: 1,
            retry_non_idempotent_methods: false,
            ..HttpConfig::default()
        })
        .unwrap();
        let response = client
            .post(&format!("http://{address}/"), "data")
            .await
            .unwrap();
        assert_eq!(
            response.status(),
            reqwest::StatusCode::INTERNAL_SERVER_ERROR
        );
        assert_eq!(attempts.load(Ordering::SeqCst), 1);
    }

    #[tokio::test]
    async fn post_non_idempotent_retried_when_enabled() {
        let attempts = Arc::new(AtomicUsize::new(0));
        let attempts_server = attempts.clone();
        let (address, server) = spawn_test_server(move |_| {
            let attempts_server = attempts_server.clone();
            async move {
                let count = attempts_server.fetch_add(1, Ordering::SeqCst) + 1;
                if count < 3 {
                    http_response(500, &[("content-length", "5")], b"error")
                } else {
                    http_response(200, &[("content-length", "2")], b"ok")
                }
            }
        })
        .await;
        tokio::spawn(server);

        let client = ScanClient::from_config(HttpConfig {
            max_retries: 4,
            retry_delay_ms: 1,
            retry_non_idempotent_methods: true,
            ..HttpConfig::default()
        })
        .unwrap();
        let response = client
            .post(&format!("http://{address}/"), "data")
            .await
            .unwrap();
        assert_eq!(response.status(), reqwest::StatusCode::OK);
        assert_eq!(attempts.load(Ordering::SeqCst), 3);
    }
}
