use super::*;

#[tokio::test]
async fn blocks_requests_to_internal_addresses_by_default() {
    let transport =
        HttpTransport::new(1, Duration::from_secs(5)).expect("operation should succeed");
    let plan = single_request_plan("http://127.0.0.1:1/blocked".to_string());

    let results = transport.execute(&plan).collect::<Vec<_>>().await;

    assert_eq!(results.len(), 1);
    let err = results
        .into_iter()
        .next()
        .expect("operation should succeed")
        .unwrap_err();
    let rendered = err.to_string();
    assert!(rendered.contains("blocked internal address resolution"));
    assert!(rendered.contains("127.0.0.1"));
}

#[tokio::test]
async fn allow_internal_option_overrides_ssrf_protection() {
    let listener = TcpListener::bind("127.0.0.1:0")
        .await
        .expect("operation should succeed");
    let addr = listener.local_addr().expect("operation should succeed");
    let server = tokio::spawn(async move {
        let (mut socket, _) = listener.accept().await.expect("operation should succeed");
        let mut buf = [0u8; 2048];
        let _ = socket
            .read(&mut buf)
            .await
            .expect("operation should succeed");
        socket
            .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
            .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 plan = single_request_plan(format!("http://{addr}/allowed"));

    let responses = transport
        .execute(&plan)
        .collect::<Vec<_>>()
        .await
        .into_iter()
        .collect::<std::result::Result<Vec<_>, secir::Error>>()
        .expect("operation should succeed");

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

    assert_eq!(responses.len(), 1);
    assert_eq!(responses[0].data.status, 200);
}

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

    let server = tokio::spawn(async move {
        let (mut socket, _) = listener.accept().await.expect("operation should succeed");
        let mut buf = [0u8; 4096];
        let n = socket
            .read(&mut buf)
            .await
            .expect("operation should succeed");
        *seen_request_ref.lock().await = String::from_utf8_lossy(&buf[..n]).to_string();
        socket
            .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
            .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 plan = single_request_plan(format!("http://{addr}/default-user-agent"));

    let responses = transport
        .execute(&plan)
        .collect::<Vec<_>>()
        .await
        .into_iter()
        .collect::<std::result::Result<Vec<_>, secir::Error>>()
        .expect("operation should succeed");

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

    let request = seen_request.lock().await.clone();
    let request_lower = request.to_ascii_lowercase();
    let expected = crate::tls_profiles::rotate(0)
        .user_agent
        .to_ascii_lowercase();
    let facts = guise_profiles::default_profile_facts();
    assert_eq!(responses.len(), 1);
    assert!(request_lower.contains("user-agent: mozilla/5.0"));
    assert!(
        request_lower.contains(&expected),
        "default transport User-Agent drifted from stealth profile: {request}"
    );
    assert!(
        request_lower.contains(&format!("accept: {}", facts.accept).to_ascii_lowercase()),
        "default transport Accept header drifted from stealth profile: {request}"
    );
    assert!(
        request_lower
            .contains(&format!("accept-language: {}", facts.accept_language).to_ascii_lowercase()),
        "default transport Accept-Language header drifted from stealth profile: {request}"
    );
    assert!(
        request_lower.contains("upgrade-insecure-requests: 1"),
        "default transport must carry shared navigation upgrade metadata: {request}"
    );
    assert!(
        request_lower.contains("sec-fetch-dest: document"),
        "default transport must carry shared navigation destination metadata: {request}"
    );
    assert!(
        request_lower.contains("sec-fetch-mode: navigate"),
        "default transport must carry shared navigation mode metadata: {request}"
    );
    assert!(
        request_lower.contains("sec-fetch-site: none"),
        "default transport must carry shared navigation site metadata: {request}"
    );
    assert!(
        request_lower.contains("sec-fetch-user: ?1"),
        "default transport must carry shared navigation user activation metadata: {request}"
    );
}

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

    let server = tokio::spawn(async move {
        let (mut socket, _) = listener.accept().await.expect("operation should succeed");
        let mut buf = [0u8; 4096];
        let n = socket
            .read(&mut buf)
            .await
            .expect("operation should succeed");
        *seen_request_ref.lock().await = String::from_utf8_lossy(&buf[..n]).to_string();
        socket
            .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
            .await
            .expect("operation should succeed");
    });

    let transport = HttpTransport::with_transport_options(
        1,
        Duration::from_secs(5),
        HttpTransportOptions {
            allow_internal: true,
            browser_profile: Some("firefox".to_string()),
            camouflage: true,
            ..HttpTransportOptions::default()
        },
    )
    .expect("operation should succeed");
    let plan = single_request_plan(format!("http://{addr}/camouflage"));

    let responses = transport
        .execute(&plan)
        .collect::<Vec<_>>()
        .await
        .into_iter()
        .collect::<std::result::Result<Vec<_>, secir::Error>>()
        .expect("operation should succeed");

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

    let request = seen_request.lock().await.clone();
    let request_lower = request.to_ascii_lowercase();
    assert_eq!(responses.len(), 1);
    assert!(request_lower.contains("user-agent: mozilla/5.0"));
    // Must match the shipped camouflage profile UA in src/tls_profiles.rs.
    assert!(request_lower.contains("firefox/133.0"));
    assert!(request_lower.contains("accept-language: en-us,en;q=0.5"));
    assert!(request_lower.contains("accept-encoding: gzip, deflate, br"));
    assert!(request_lower.contains("sec-fetch-mode: navigate"));
    assert!(request_lower.contains("sec-fetch-dest: document"));
    assert!(request_lower.contains("sec-fetch-user: ?1"));
}

#[tokio::test]
async fn rejects_chunked_body_that_exceeds_max_response_size() {
    let listener = TcpListener::bind("127.0.0.1:0")
        .await
        .expect("operation should succeed");
    let addr = listener.local_addr().expect("operation should succeed");
    let oversized_body = vec![b'a'; 10 * 1024 * 1024];
    let response = Arc::new(chunked_http_response(100, &oversized_body));

    let server = tokio::spawn({
        let response = Arc::clone(&response);
        async move {
            let (mut socket, _) = listener.accept().await.expect("operation should succeed");
            let mut buf = [0u8; 2048];
            let _ = socket
                .read(&mut buf)
                .await
                .expect("operation should succeed");
            let _ = socket.write_all(response.as_slice()).await;
        }
    });

    let transport = HttpTransport::with_transport_options(
        1,
        Duration::from_secs(5),
        HttpTransportOptions {
            max_response_size: Some(1024 * 1024),
            allow_internal: true,
            ..HttpTransportOptions::default()
        },
    )
    .expect("operation should succeed");
    let plan = single_request_plan(format!("http://{addr}/chunked"));

    let results = transport.execute(&plan).collect::<Vec<_>>().await;

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

    assert_eq!(results.len(), 1);
    let err = results
        .into_iter()
        .next()
        .expect("operation should succeed")
        .unwrap_err();
    assert!(err.to_string().contains("response body too large"));
}

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

    let server = tokio::spawn(async move {
        for _ in 0..2 {
            let (mut socket, _) = listener.accept().await.expect("operation should succeed");
            let seen_attempts = Arc::clone(&seen_attempts);
            tokio::spawn(async move {
                let mut buf = [0u8; 2048];
                let _ = socket
                    .read(&mut buf)
                    .await
                    .expect("operation should succeed");
                let mut attempts = seen_attempts.lock().await;
                *attempts += 1;
                if *attempts == 1 {
                    // Simulate an abrupt connection failure to trigger an Err response
                    drop(socket);
                } else {
                    let response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok";
                    socket
                        .write_all(response.as_bytes())
                        .await
                        .expect("operation should succeed");
                };
            });
        }
    });

    let transport = HttpTransport::with_transport_options(
        1,
        Duration::from_secs(5),
        HttpTransportOptions {
            retries: 2,
            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}/retry"),
            method: "GET".to_string(),
            headers: HashMap::new(),
            body: None,
            template_refs: vec![("retry-template".to_string(), 0)],
            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::<std::result::Result<Vec<_>, secir::Error>>()
        .expect("operation should succeed");

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

    assert_eq!(responses.len(), 1);
    assert_eq!(responses[0].data.status, 200);
    assert_eq!(*attempts.lock().await, 2);
}

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

    let server = tokio::spawn(async move {
        for _ in 0..3 {
            let (mut socket, _) = listener.accept().await.expect("operation should succeed");
            let seen_attempts = Arc::clone(&seen_attempts);
            tokio::spawn(async move {
                let mut buf = [0u8; 2048];
                let _ = socket
                    .read(&mut buf)
                    .await
                    .expect("operation should succeed");
                let mut attempts = seen_attempts.lock().await;
                *attempts += 1;
                if *attempts < 3 {
                    // Simulate an abrupt connection failure to trigger an Err response
                    drop(socket);
                } else {
                    let response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok";
                    socket
                        .write_all(response.as_bytes())
                        .await
                        .expect("operation should succeed");
                };
            });
        }
    });

    let transport = HttpTransport::with_transport_options(
        1,
        Duration::from_secs(5),
        HttpTransportOptions {
            retries: 3,
            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}/retry"),
            method: "GET".to_string(),
            headers: HashMap::new(),
            body: None,
            template_refs: vec![("retry-template".to_string(), 0)],
            follow_redirects: true,
            max_redirects: 10,
            cookie_reuse: true,
        }],
        target_count: 1,
        template_count: 1,
        dedup_savings: 0,
    };

    let started = std::time::Instant::now();
    let responses = transport
        .execute(&plan)
        .collect::<Vec<_>>()
        .await
        .into_iter()
        .collect::<std::result::Result<Vec<_>, secir::Error>>()
        .expect("operation should succeed");

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

    assert_eq!(responses.len(), 1);
    assert_eq!(responses[0].data.status, 200);
    assert!(started.elapsed() >= Duration::from_millis(300));
}

#[tokio::test]
async fn max_host_errors_skips_remaining_requests_for_blocked_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 server = tokio::spawn(async move {
        let (mut socket, _) = listener.accept().await.expect("operation should succeed");
        let mut buf = [0u8; 2048];
        let _ = socket
            .read(&mut buf)
            .await
            .expect("operation should succeed");
        drop(socket);
    });

    let transport = HttpTransport::with_transport_options(
        1,
        Duration::from_millis(250),
        HttpTransportOptions {
            allow_internal: true,
            ..HttpTransportOptions::default()
        },
    )
    .expect("operation should succeed")
    .with_max_host_errors(1, true);
    let base = format!("http://{addr}");
    let plan = RequestPlan {
        requests: vec![
            RequestSpec {
                target: base.clone(),
                url: format!("{base}/first"),
                method: "GET".to_string(),
                headers: HashMap::new(),
                body: None,
                template_refs: vec![("blocked-host".to_string(), 0)],
                follow_redirects: true,
                max_redirects: 10,
                cookie_reuse: true,
            },
            RequestSpec {
                target: base.clone(),
                url: format!("{base}/second"),
                method: "GET".to_string(),
                headers: HashMap::new(),
                body: None,
                template_refs: vec![("blocked-host".to_string(), 1)],
                follow_redirects: true,
                max_redirects: 10,
                cookie_reuse: true,
            },
        ],
        target_count: 1,
        template_count: 1,
        dedup_savings: 0,
    };

    let results = transport.execute(&plan).collect::<Vec<_>>().await;
    server.await.expect("operation should succeed");

    assert_eq!(results.len(), 2);
    let first_err = results[0]
        .as_ref()
        .expect_err("request should fail")
        .to_string();
    let second_err = results[1]
        .as_ref()
        .expect_err("request should be skipped")
        .to_string();
    let host = host_key(&format!("{base}/first")).expect("host key should resolve");

    assert!(!first_err.contains("--max-host-errors"));
    assert!(second_err.contains("skipping host after reaching --max-host-errors threshold"));
    assert!(transport.is_host_blocked(&host));
}

#[tokio::test]
async fn fixed_delay_spaces_requests() {
    let listener = TcpListener::bind("127.0.0.1:0")
        .await
        .expect("operation should succeed");
    let addr = listener.local_addr().expect("operation should succeed");

    let server = tokio::spawn(async move {
        for _ in 0..2 {
            let (mut socket, _) = listener.accept().await.expect("operation should succeed");
            tokio::spawn(async move {
                let mut buf = [0u8; 2048];
                let _ = socket
                    .read(&mut buf)
                    .await
                    .expect("operation should succeed");
                socket
                    .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
                    .await
                    .expect("operation should succeed");
            });
        }
    });

    let transport = HttpTransport::with_transport_options(
        1,
        Duration::from_secs(5),
        HttpTransportOptions {
            fixed_delay: Some(Duration::from_millis(100)),
            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}/one"),
                method: "GET".to_string(),
                headers: HashMap::new(),
                body: None,
                template_refs: vec![("delay-template".to_string(), 0)],
                follow_redirects: true,
                max_redirects: 10,
                cookie_reuse: true,
            },
            RequestSpec {
                target: base.clone(),
                url: format!("{base}/two"),
                method: "GET".to_string(),
                headers: HashMap::new(),
                body: None,
                template_refs: vec![("delay-template".to_string(), 1)],
                follow_redirects: true,
                max_redirects: 10,
                cookie_reuse: true,
            },
        ],
        target_count: 1,
        template_count: 1,
        dedup_savings: 0,
    };

    let started = std::time::Instant::now();
    let responses = transport
        .execute(&plan)
        .collect::<Vec<_>>()
        .await
        .into_iter()
        .collect::<std::result::Result<Vec<_>, secir::Error>>()
        .expect("operation should succeed");

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

    assert_eq!(responses.len(), 2);
    assert!(started.elapsed() >= Duration::from_millis(100));
}

#[tokio::test]
async fn request_coordinator_notifies_waiters_after_completion() {
    let coordinator = Arc::new(RequestCoordinator::default());
    let waiting_spec = RequestSpec {
        target: "http://example.com".to_string(),
        url: "http://example.com/admin".to_string(),
        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,
    };
    let completed_spec = RequestSpec {
        template_refs: vec![("template-a".to_string(), 0)],
        ..waiting_spec.clone()
    };

    let waiter = {
        let coordinator = Arc::clone(&coordinator);
        let waiting_spec = waiting_spec.clone();
        tokio::spawn(async move {
            tokio::time::timeout(
                Duration::from_secs(1),
                coordinator.wait_for_predecessors(&waiting_spec),
            )
            .await
        })
    };

    tokio::time::sleep(Duration::from_millis(20)).await;
    coordinator.mark_completed(&completed_spec).await;

    assert!(waiter.await.expect("operation should succeed").is_ok());
}
