#![allow(clippy::too_many_lines)]
use super::*;
use std::collections::BTreeMap;

#[test]
fn internal_ip_filter_blocks_shared_and_benchmark_ranges() {
    assert!(is_internal_ip(
        "100.64.0.1"
            .parse()
            .expect("100.64.0.1 is a valid IPv4 address")
    ));
    assert!(is_internal_ip(
        "100.127.255.254"
            .parse()
            .expect("100.127.255.254 is a valid IPv4 address")
    ));
    assert!(is_internal_ip(
        "198.18.0.1"
            .parse()
            .expect("198.18.0.1 is a valid IPv4 address")
    ));
    assert!(is_internal_ip(
        "198.19.255.254"
            .parse()
            .expect("198.19.255.254 is a valid IPv4 address")
    ));
    assert!(!is_internal_ip(
        "198.20.0.1"
            .parse()
            .expect("198.20.0.1 is a valid IPv4 address")
    ));
}

#[test]
fn custom_sni_returns_error_instead_of_silently_nooping() {
    let result = HttpTransport::with_transport_options(
        1,
        Duration::from_secs(1),
        HttpTransportOptions {
            sni: Some("scan.example".to_string()),
            ..HttpTransportOptions::default()
        },
    );

    match result {
        Ok(_) => panic!("custom SNI should fail fast until reqwest exposes support"),
        Err(Error::Transport { message, .. }) => {
            assert!(message.contains("custom SNI override"));
            assert!(message.contains("not supported"));
        }
        Err(other) => panic!("unexpected error type: {other}"),
    }
}

#[tokio::test]
async fn persists_cookies_across_multi_request_templates() {
    let listener = TcpListener::bind("127.0.0.1:0")
        .await
        .expect("operation should succeed");
    let addr = listener.local_addr().expect("operation should succeed");
    let requests = Arc::new(AsyncMutex::new(Vec::new()));
    let seen = Arc::clone(&requests);

    let server = tokio::spawn(async move {
        for _ in 0..2 {
            let (mut socket, _) = listener.accept().await.expect("operation should succeed");
            let seen = Arc::clone(&seen);
            tokio::spawn(async move {
                let mut buf = [0u8; 4096];
                let n = socket
                    .read(&mut buf)
                    .await
                    .expect("operation should succeed");
                let request = String::from_utf8_lossy(&buf[..n]).to_string();
                seen.lock().await.push(request.clone());

                let response = if request.starts_with("POST /login ") {
                    "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nSet-Cookie: session=abc123; Path=/\r\n\r\nok"
                        .to_string()
                } else if request.starts_with("GET /admin ")
                    && request
                        .to_ascii_lowercase()
                        .contains("cookie: session=abc123")
                {
                    "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nadmin".to_string()
                } else {
                    "HTTP/1.1 403 Forbidden\r\nContent-Length: 9\r\n\r\nforbidden".to_string()
                };

                socket
                    .write_all(response.as_bytes())
                    .await
                    .expect("operation should succeed");
            });
        }
    });

    let transport = HttpTransport::with_transport_options(
        10,
        Duration::from_secs(5),
        HttpTransportOptions {
            allow_internal: true,
            ..HttpTransportOptions::default()
        },
    )
    .expect("operation should succeed");
    let base = format!("http://{addr}");
    let plan = RequestPlan {
        requests: vec![
            RequestSpec {
                target: base.clone(),
                url: format!("{base}/login"),
                method: "POST".to_string(),
                headers: HashMap::new(),
                body: Some(b"username=admin&password=admin".to_vec()),
                template_refs: vec![("auth-template".to_string(), 0)],
                follow_redirects: true,
                max_redirects: 10,
                cookie_reuse: true,
            },
            RequestSpec {
                target: base.clone(),
                url: format!("{base}/admin"),
                method: "GET".to_string(),
                headers: HashMap::new(),
                body: None,
                template_refs: vec![("auth-template".to_string(), 1)],
                follow_redirects: true,
                max_redirects: 10,
                cookie_reuse: true,
            },
        ],
        target_count: 1,
        template_count: 1,
        dedup_savings: 0,
    };

    let responses = transport
        .execute(&plan)
        .collect::<Vec<_>>()
        .await
        .into_iter()
        .collect::<Result<Vec<_>>>()
        .expect("operation should succeed");
    let mut statuses = responses
        .iter()
        .map(|response| response.data.status)
        .collect::<Vec<_>>();
    statuses.sort_unstable();

    server.await.expect("operation should succeed");

    assert_eq!(statuses, vec![200, 200]);
    let recorded = requests.lock().await;
    assert!(
        recorded
            .iter()
            .any(|request| request.starts_with("POST /login "))
    );
    assert!(recorded.iter().any(|request| {
        request.starts_with("GET /admin ")
            && request
                .to_ascii_lowercase()
                .contains("cookie: session=abc123")
    }));
}

#[tokio::test]
async fn cookies_are_isolated_between_templates_on_the_same_host() {
    let listener = TcpListener::bind("127.0.0.1:0")
        .await
        .expect("operation should succeed");
    let addr = listener.local_addr().expect("operation should succeed");
    let requests = Arc::new(AsyncMutex::new(Vec::new()));
    let seen = Arc::clone(&requests);

    let server = tokio::spawn(async move {
        for _ in 0..3 {
            let (mut socket, _) = listener.accept().await.expect("operation should succeed");
            let seen = Arc::clone(&seen);
            tokio::spawn(async move {
                let mut buf = [0u8; 4096];
                let n = socket
                    .read(&mut buf)
                    .await
                    .expect("operation should succeed");
                let request = String::from_utf8_lossy(&buf[..n]).to_string();
                seen.lock().await.push(request.clone());

                let response = if request.starts_with("POST /login ") {
                    "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nSet-Cookie: session=abc123; Path=/\r\n\r\nok"
                        .to_string()
                } else if request.starts_with("GET /isolation-check ") {
                    let has_cookie = request
                        .to_ascii_lowercase()
                        .contains("cookie: session=abc123");
                    let body = if has_cookie { "leaked" } else { "clean" };
                    format!(
                        "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{body}",
                        body.len()
                    )
                } else if request.starts_with("GET /template-a-check ")
                    && request
                        .to_ascii_lowercase()
                        .contains("cookie: session=abc123")
                {
                    "HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\npresent".to_string()
                } else {
                    "HTTP/1.1 403 Forbidden\r\nContent-Length: 9\r\n\r\nforbidden".to_string()
                };

                socket
                    .write_all(response.as_bytes())
                    .await
                    .expect("operation should succeed");
            });
        }
    });

    let transport = HttpTransport::with_transport_options(
        1,
        Duration::from_secs(5),
        HttpTransportOptions {
            allow_internal: true,
            ..HttpTransportOptions::default()
        },
    )
    .expect("operation should succeed");
    let base = format!("http://{addr}");
    let plan = RequestPlan {
        requests: vec![
            RequestSpec {
                target: base.clone(),
                url: format!("{base}/login"),
                method: "POST".to_string(),
                headers: HashMap::new(),
                body: Some(b"username=admin&password=admin".to_vec()),
                template_refs: vec![("template-a".to_string(), 0)],
                follow_redirects: true,
                max_redirects: 10,
                cookie_reuse: true,
            },
            RequestSpec {
                target: base.clone(),
                url: format!("{base}/isolation-check"),
                method: "GET".to_string(),
                headers: HashMap::new(),
                body: None,
                template_refs: vec![("template-b".to_string(), 0)],
                follow_redirects: true,
                max_redirects: 10,
                cookie_reuse: true,
            },
            RequestSpec {
                target: base.clone(),
                url: format!("{base}/template-a-check"),
                method: "GET".to_string(),
                headers: HashMap::new(),
                body: None,
                template_refs: vec![("template-a".to_string(), 1)],
                follow_redirects: true,
                max_redirects: 10,
                cookie_reuse: true,
            },
        ],
        target_count: 1,
        template_count: 2,
        dedup_savings: 0,
    };

    let responses = transport
        .execute(&plan)
        .collect::<Vec<_>>()
        .await
        .into_iter()
        .collect::<Result<Vec<_>>>()
        .expect("operation should succeed");
    let mut bodies = responses
        .iter()
        .map(|response| String::from_utf8_lossy(&response.data.body).to_string())
        .collect::<Vec<_>>();
    bodies.sort();

    server.await.expect("operation should succeed");

    assert_eq!(bodies, vec!["clean", "ok", "present"]);
    let recorded = requests.lock().await;
    assert!(recorded.iter().any(|request| {
        request.starts_with("GET /isolation-check ")
            && !request
                .to_ascii_lowercase()
                .contains("cookie: session=abc123")
    }));
    assert!(recorded.iter().any(|request| {
        request.starts_with("GET /template-a-check ")
            && request
                .to_ascii_lowercase()
                .contains("cookie: session=abc123")
    }));
}

#[test]
fn cookie_parser_uses_name_value_pair() {
    assert_eq!(
        parse_set_cookie("session=abc123; Path=/; HttpOnly"),
        Some((
            "session".to_string(),
            "abc123".to_string(),
            Some("/".to_string())
        ))
    );
    assert_eq!(parse_set_cookie("invalid-cookie"), None);
}

#[test]
fn cookie_header_only_returns_matching_path() {
    let jar = CookieJar::default();
    let req_a = RequestSpec {
        target: "http://example.com".to_string(),
        url: "http://example.com/login".to_string(),
        method: "GET".to_string(),
        headers: HashMap::new(),
        body: None,
        template_refs: vec![("template-a".to_string(), 0)],
        follow_redirects: true,
        max_redirects: 10,
        cookie_reuse: true,
    };
    let req_b = RequestSpec {
        url: "http://example.com/admin".to_string(),
        ..req_a.clone()
    };

    let host = host_key(&req_a.url).expect("operation should succeed");
    let mut cookies = jar.cookies.lock();
    cookies.insert(
        CookieScope {
            template_id: "template-a".to_string(),
            host: host.clone(),
            path: "/login".to_string(),
        },
        BTreeMap::from([("session".to_string(), "login-cookie".to_string())]),
    );
    cookies.insert(
        CookieScope {
            template_id: "template-a".to_string(),
            host,
            path: "/admin".to_string(),
        },
        BTreeMap::from([("session".to_string(), "admin-cookie".to_string())]),
    );
    drop(cookies);

    assert_eq!(
        jar.cookie_header(&req_a).as_deref(),
        Some("session=login-cookie")
    );
    assert_eq!(
        jar.cookie_header(&req_b).as_deref(),
        Some("session=admin-cookie")
    );
}

#[test]
fn cookie_header_does_not_leak_between_paths_on_same_host() {
    let jar = CookieJar::default();
    let req_login = RequestSpec {
        target: "http://example.com".to_string(),
        url: "http://example.com/login".to_string(),
        method: "GET".to_string(),
        headers: HashMap::new(),
        body: None,
        template_refs: vec![("auth-A".to_string(), 0)],
        follow_redirects: true,
        max_redirects: 10,
        cookie_reuse: true,
    };
    let req_admin = RequestSpec {
        url: "http://example.com/admin".to_string(),
        template_refs: vec![("auth-B".to_string(), 0)],
        ..req_login.clone()
    };

    let host = host_key(&req_login.url).expect("operation should succeed");
    let mut cookies = jar.cookies.lock();
    cookies.insert(
        CookieScope {
            template_id: "auth-A".to_string(),
            host,
            path: "/login".to_string(),
        },
        BTreeMap::from([("session".to_string(), "abc".to_string())]),
    );
    drop(cookies);

    assert_eq!(jar.cookie_header(&req_admin), None);
}

#[test]
fn with_options_stores_proxy_url() {
    let proxy_url = "http://127.0.0.1:8080".to_string();

    let transport = HttpTransport::with_options(
        10,
        Duration::from_secs(5),
        None,
        DEFAULT_MAX_RESPONSE_SIZE,
        true,
        Some(proxy_url.clone()),
    )
    .expect("operation should succeed");

    assert_eq!(transport.proxy_url(), Some(proxy_url.as_str()));
}

#[test]
fn with_transport_options_stores_proxy_rotation_list() {
    let transport = HttpTransport::with_transport_options(
        10,
        Duration::from_secs(5),
        HttpTransportOptions {
            proxy_urls: vec![
                "http://127.0.0.1:8080".to_string(),
                "http://127.0.0.1:8081".to_string(),
            ],
            ..HttpTransportOptions::default()
        },
    )
    .expect("operation should succeed");

    assert_eq!(
        transport.proxy_urls(),
        &[
            "http://127.0.0.1:8080".to_string(),
            "http://127.0.0.1:8081".to_string()
        ]
    );
    assert_eq!(
        transport.proxy_url_for_request_index(0),
        Some("http://127.0.0.1:8080")
    );
    assert_eq!(
        transport.proxy_url_for_request_index(1),
        Some("http://127.0.0.1:8081")
    );
    assert_eq!(
        transport.proxy_url_for_request_index(2),
        Some("http://127.0.0.1:8080")
    );
}

#[test]
fn proxy_url_for_request_index_falls_back_to_fixed_proxy() {
    let transport = HttpTransport::with_transport_options(
        10,
        Duration::from_secs(5),
        HttpTransportOptions {
            proxy_url: Some("http://127.0.0.1:8080".to_string()),
            ..HttpTransportOptions::default()
        },
    )
    .expect("operation should succeed");

    assert_eq!(
        transport.proxy_url_for_request_index(4),
        Some("http://127.0.0.1:8080")
    );
}

#[test]
fn with_options_and_retries_stores_retry_count() {
    let transport = HttpTransport::with_options_and_retries(
        10,
        Duration::from_secs(5),
        None,
        DEFAULT_MAX_RESPONSE_SIZE,
        true,
        None,
        4,
    )
    .expect("operation should succeed");

    assert_eq!(transport.retries(), 4);
}

#[test]
fn with_options_stores_proxy_auth_delay_and_retries() {
    let transport = HttpTransport::with_transport_options(
        10,
        Duration::from_secs(5),
        HttpTransportOptions {
            fixed_delay: Some(Duration::from_millis(25)),
            proxy_url: Some("http://127.0.0.1:8080".to_string()),
            proxy_auth: Some(("alice".to_string(), "secret".to_string())),
            retries: 3,
            ..HttpTransportOptions::default()
        },
    )
    .expect("operation should succeed");

    assert_eq!(transport.proxy_auth(), Some(("alice", "secret")));
    assert_eq!(transport.fixed_delay(), Some(Duration::from_millis(25)));
    assert_eq!(transport.retries(), 3);
}
