//! Fingerprint integration tests.
//!
//! These tests verify our fingerprints don't match known automation tool signatures
//! and correctly emulate Chrome browser fingerprints.
//!
//! Tests against:
//! - tls.peet.ws (TLS/HTTP/2 fingerprint validation)
//! - tls.browserleaks.com (TLS fingerprint validation)
//!
//! Run with: cargo test --test fingerprint_integration

use http::{Method, Uri};
use specter::fingerprint::http2::Http2Settings;
use specter::fingerprint::tls::TlsFingerprint;
use specter::transport::connector::BoringConnector;
use specter::transport::h2::{H2Connection, PseudoHeaderOrder};
use specter::transport::h3::H3Client;
use tracing::warn;

/// Known automation tool fingerprints that we MUST NOT match
const KNOWN_JA3_PYTHON_REQUESTS: &str = "8d9f7747675e24454cd9b7ed35c58707";
const KNOWN_JA3_CURL_7X: &str = "e7d705a3286e19ea42f587b344ee6865";

/// Expected Chrome HTTP/2 Akamai fingerprint components
const CHROME_AKAMAI_SETTINGS: &str = "1:65536;2:0;3:1000;4:6291456;5:16384;6:262144";
const CHROME_WINDOW_UPDATE: &str = "15663105";
const CHROME_PSEUDO_ORDER: &str = "m,s,a,p";

// Note: Now that we send PRIORITY frames, the priority field contains the actual
// priority tree. The weights are stored as-is (not weight-1), so 201 becomes 202
// in the Akamai format (which adds 1 when displaying).
// Format: stream:exclusive:dependency:weight+1

/// Expected Firefox HTTP/2 Akamai fingerprint components
const FIREFOX_AKAMAI_SETTINGS: &str = "1:65536;4:131072;5:16384";
const FIREFOX_WINDOW_UPDATE: &str = "12517377";
const FIREFOX_PSEUDO_ORDER: &str = "m,p,a,s";

#[tokio::test]
async fn test_tls_fingerprint_unique() {
    let fp = TlsFingerprint::chrome_142();
    let connector = BoringConnector::with_fingerprint(fp);
    let uri: Uri = "https://tls.peet.ws/api/all".parse().unwrap();

    let stream = connector
        .connect(&uri)
        .await
        .expect("TLS connection should succeed");

    // Verify ALPN negotiated h2
    assert!(stream.is_h2(), "Should negotiate HTTP/2 via ALPN");
}

#[tokio::test]
async fn test_http2_fingerprint_matches_chrome() {
    let fp = TlsFingerprint::chrome_142();
    let connector = BoringConnector::with_fingerprint(fp);
    let settings = Http2Settings::default();
    let uri: Uri = "https://tls.peet.ws/api/all".parse().unwrap();

    let stream = connector
        .connect(&uri)
        .await
        .expect("TLS connection should succeed");

    if !stream.is_h2() {
        panic!("Server did not negotiate HTTP/2");
    }

    let mut h2_conn = H2Connection::connect(stream, settings, PseudoHeaderOrder::Chrome)
        .await
        .expect("HTTP/2 connection should succeed");

    let headers = vec![
        (
            "user-agent".to_string(),
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36".to_string(),
        ),
        ("accept".to_string(), "application/json".to_string()),
    ];

    let response = h2_conn
        .send_request(Method::GET, &uri, headers, None)
        .await
        .expect("HTTP/2 request should succeed");

    assert_eq!(response.status().as_u16(), 200, "Should get 200 OK");

    // Parse response JSON
    let body = String::from_utf8_lossy(response.body());
    let json: serde_json::Value =
        serde_json::from_str(&body).expect("Response should be valid JSON");

    // Validate HTTP/2 fingerprint
    if let Some(h2_fp) = json.get("http2") {
        // Check Akamai fingerprint
        if let Some(akamai) = h2_fp.get("akamai_fingerprint") {
            let akamai_str = akamai.as_str().unwrap();
            let parts: Vec<&str> = akamai_str.split('|').collect();

            assert_eq!(parts.len(), 4, "Akamai fingerprint should have 4 parts");

            // Strip GREASE settings (e.g., ":0" suffix) before comparing
            // GREASE settings are random and vary per connection
            let settings_parts: Vec<&str> = parts[0]
                .split(';')
                .filter(|s| {
                    s.starts_with("1:")
                        || s.starts_with("2:")
                        || s.starts_with("3:")
                        || s.starts_with("4:")
                        || s.starts_with("5:")
                        || s.starts_with("6:")
                })
                .collect();
            let normalized_settings = settings_parts.join(";");
            assert_eq!(
                normalized_settings, CHROME_AKAMAI_SETTINGS,
                "SETTINGS should match Chrome (GREASE stripped)"
            );

            assert_eq!(
                parts[1], CHROME_WINDOW_UPDATE,
                "WINDOW_UPDATE should match Chrome"
            );
            // PRIORITY frames are now sent, so this field contains actual priority data
            // Format: stream:exclusive:dependency:weight
            // Chrome sends 5 PRIORITY frames for streams 3,5,7,9,11
            let priority_str = parts[2];
            if priority_str != "0" {
                // Verify we're sending PRIORITY frames as expected
                assert!(
                    priority_str.contains("3:") && priority_str.contains("5:"),
                    "Priority field should contain Chrome priority tree"
                );
            }
            assert_eq!(
                parts[3], CHROME_PSEUDO_ORDER,
                "Pseudo-header order should match Chrome"
            );
        } else {
            panic!("Response should include akamai_fingerprint");
        }

        // Check Akamai hash - note that different services may calculate hashes differently
        // or include GREASE settings in the hash calculation, so we verify it doesn't match
        // known automation tool fingerprints rather than requiring an exact match
        if let Some(hash) = h2_fp.get("akamai_fingerprint_hash") {
            let hash_str = hash.as_str().unwrap();
            // Verify it doesn't match known automation tool hashes
            assert_ne!(hash_str, "", "Akamai hash should be present");
            // The hash may vary between services due to GREASE, so we just verify it's present
            // The exact hash match is validated against browserleaks.com in test_browserleaks_passes
        }

        // Validate sent frames
        if let Some(frames) = h2_fp.get("sent_frames").and_then(|f| f.as_array()) {
            // Frame 0: SETTINGS with 6+ parameters (may include GREASE)
            if let Some(settings_frame) = frames.first() {
                assert_eq!(settings_frame["frame_type"], "SETTINGS");
                let settings_list = settings_frame["settings"].as_array().unwrap();
                // Chrome sends 6 core settings, plus potentially GREASE settings
                assert!(
                    settings_list.len() >= 6,
                    "Should send at least 6 SETTINGS parameters (may include GREASE)"
                );

                // Verify settings order and values
                assert_eq!(settings_list[0], "HEADER_TABLE_SIZE = 65536");
                assert_eq!(settings_list[1], "ENABLE_PUSH = 0");
                assert_eq!(settings_list[2], "MAX_CONCURRENT_STREAMS = 1000");
                assert_eq!(settings_list[3], "INITIAL_WINDOW_SIZE = 6291456");
                assert_eq!(settings_list[4], "MAX_FRAME_SIZE = 16384");
                assert_eq!(settings_list[5], "MAX_HEADER_LIST_SIZE = 262144");
            }

            // Frame 1: WINDOW_UPDATE
            if let Some(wu_frame) = frames.get(1) {
                assert_eq!(wu_frame["frame_type"], "WINDOW_UPDATE");
                assert_eq!(wu_frame["increment"], 15663105);
            }

            // HEADERS frame with correct pseudo-order
            // Note: With PRIORITY frames, HEADERS is no longer at index 3
            // Find the HEADERS frame dynamically
            let headers_frame = frames
                .iter()
                .find(|f| f.get("frame_type").and_then(|v| v.as_str()) == Some("HEADERS"));
            if let Some(headers_frame) = headers_frame {
                let headers = headers_frame["headers"].as_array().unwrap();

                // Verify pseudo-header order: m,s,a,p
                assert_eq!(headers[0], ":method: GET");
                assert_eq!(headers[1], ":scheme: https");
                assert_eq!(headers[2], ":authority: tls.peet.ws");
                assert_eq!(headers[3], ":path: /api/all");
            }
        }
    }

    // Validate TLS fingerprint
    if let Some(tls_fp) = json.get("tls") {
        if let Some(ja3) = tls_fp.get("ja3_hash") {
            let ja3_str = ja3.as_str().unwrap();

            assert_ne!(
                ja3_str, KNOWN_JA3_PYTHON_REQUESTS,
                "JA3 should NOT match Python requests automation tool fingerprint"
            );
            assert_ne!(
                ja3_str, KNOWN_JA3_CURL_7X,
                "JA3 should NOT match cURL 7.x automation tool fingerprint"
            );
        }
    }
}

#[tokio::test]
async fn test_browserleaks_passes() {
    let fp = TlsFingerprint::chrome_142();
    let connector = BoringConnector::with_fingerprint(fp);
    let settings = Http2Settings::default();
    let uri: Uri = "https://tls.browserleaks.com/json".parse().unwrap();

    let stream = connector
        .connect(&uri)
        .await
        .expect("TLS connection should succeed");

    if !stream.is_h2() {
        warn!("WARNING: Server did not negotiate HTTP/2, skipping test");
        return;
    }

    let mut h2_conn = H2Connection::connect(stream, settings, PseudoHeaderOrder::Chrome)
        .await
        .expect("HTTP/2 connection should succeed");

    let headers = vec![
        ("user-agent".to_string(), "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36".to_string()),
        ("accept".to_string(), "application/json".to_string()),
        ("accept-language".to_string(), "en-US,en;q=0.9".to_string()),
    ];

    let response = h2_conn
        .send_request(Method::GET, &uri, headers, None)
        .await
        .expect("browserleaks.com should accept our fingerprint");

    assert_eq!(
        response.status().as_u16(),
        200,
        "browserleaks.com should return 200 OK"
    );

    // Parse and validate response
    let body = String::from_utf8_lossy(response.body());
    let json: serde_json::Value =
        serde_json::from_str(&body).expect("Response should be valid JSON");

    // Verify JA3 doesn't match automation tool fingerprints
    if let Some(ja3) = json.get("ja3_hash") {
        let ja3_str = ja3.as_str().unwrap();
        assert_ne!(ja3_str, KNOWN_JA3_PYTHON_REQUESTS);
        assert_ne!(ja3_str, KNOWN_JA3_CURL_7X);
    }

    // Verify Akamai hash is present (exact hash varies due to GREASE and PRIORITY frames)
    if let Some(akamai_hash) = json.get("akamai_hash") {
        let hash = akamai_hash.as_str().unwrap();
        assert!(!hash.is_empty(), "Akamai hash should be present");
    }
}

#[tokio::test]
#[ignore = "HTTP/3 test flaky on macOS - QUIC socket issues"]
async fn test_http3_fingerprint_works() {
    let fp = TlsFingerprint::chrome_142();
    let h3_client = H3Client::with_fingerprint(fp);

    // Test against Cloudflare (known HTTP/3 support)
    let response = h3_client
        .send_request(
            "https://cloudflare.com/cdn-cgi/trace",
            "GET",
            vec![
                (
                    "user-agent".to_string(),
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
                        .to_string(),
                ),
                ("accept".to_string(), "*/*".to_string()),
            ],
            None,
        )
        .await
        .expect("HTTP/3 request should succeed");

    assert_eq!(response.status().as_u16(), 200);
    assert_eq!(response.http_version(), "HTTP/3");

    // Verify trace shows http/3
    let body = String::from_utf8_lossy(response.body());
    assert!(
        body.contains("http=http/3"),
        "Cloudflare trace should confirm HTTP/3"
    );
}

#[test]
fn test_settings_frame_serialization() {
    use specter::transport::h2::{SettingsFrame, SettingsId};

    let settings = Http2Settings::default();
    let mut frame = SettingsFrame::new();
    frame
        .set(SettingsId::HeaderTableSize, settings.header_table_size)
        .set(
            SettingsId::EnablePush,
            if settings.enable_push { 1 } else { 0 },
        )
        .set(
            SettingsId::MaxConcurrentStreams,
            settings.max_concurrent_streams,
        )
        .set(SettingsId::InitialWindowSize, settings.initial_window_size)
        .set(SettingsId::MaxFrameSize, settings.max_frame_size)
        .set(SettingsId::MaxHeaderListSize, settings.max_header_list_size);

    let bytes = frame.serialize();

    // Should be 9-byte header + 36-byte payload (6 settings * 6 bytes)
    assert_eq!(bytes.len(), 45, "SETTINGS frame should be 45 bytes total");

    // Verify payload size
    let length = u32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]);
    assert_eq!(length, 36, "Payload should be 36 bytes");
}

#[test]
fn test_goaway_graceful_shutdown() {
    use bytes::Bytes;
    use specter::transport::h2::{ErrorCode, GoAwayFrame, FRAME_HEADER_SIZE};

    // Server sends GOAWAY with NoError and last_stream_id=1
    let goaway = GoAwayFrame::new(1, ErrorCode::NoError);
    let full_frame = goaway.serialize();

    // Parse expects payload only (skip 9-byte header)
    let payload = Bytes::from(full_frame[FRAME_HEADER_SIZE..].to_vec());
    let parsed = GoAwayFrame::parse(payload).unwrap();

    assert_eq!(parsed.last_stream_id, 1);
    assert_eq!(parsed.error_code, ErrorCode::NoError);

    // This means stream 1 is allowed to complete normally
    // Our implementation should continue reading stream 1, not error
}

#[tokio::test]
async fn test_firefox_tls_fingerprint_unique() {
    let fp = TlsFingerprint::firefox_133();
    let connector = BoringConnector::with_fingerprint(fp);
    let uri: Uri = "https://tls.peet.ws/api/all".parse().unwrap();

    let stream = connector
        .connect(&uri)
        .await
        .expect("Firefox TLS connection should succeed");

    // Verify ALPN negotiated h2
    assert!(stream.is_h2(), "Should negotiate HTTP/2 via ALPN");

    // Verify Firefox does NOT use GREASE (check TLS fingerprint)
    let fp_check = TlsFingerprint::firefox_133();
    assert!(!fp_check.grease, "Firefox should NOT use GREASE");
}

#[tokio::test]
async fn test_firefox_http2_fingerprint_matches() {
    let fp = TlsFingerprint::firefox_133();
    let connector = BoringConnector::with_fingerprint(fp);
    let settings = Http2Settings::firefox();
    let uri: Uri = "https://tls.peet.ws/api/all".parse().unwrap();

    let stream = connector
        .connect(&uri)
        .await
        .expect("TLS connection should succeed");

    if !stream.is_h2() {
        panic!("Server did not negotiate HTTP/2");
    }

    let mut h2_conn = H2Connection::connect(stream, settings.clone(), PseudoHeaderOrder::Firefox)
        .await
        .expect("HTTP/2 connection should succeed");

    let headers = vec![
        (
            "user-agent".to_string(),
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0"
                .to_string(),
        ),
        ("accept".to_string(), "application/json".to_string()),
    ];

    let response = h2_conn
        .send_request(Method::GET, &uri, headers, None)
        .await
        .expect("HTTP/2 request should succeed");

    assert_eq!(response.status().as_u16(), 200, "Should get 200 OK");

    // Parse response JSON
    let body = String::from_utf8_lossy(response.body());
    let json: serde_json::Value =
        serde_json::from_str(&body).expect("Response should be valid JSON");

    // Validate HTTP/2 fingerprint
    if let Some(h2_fp) = json.get("http2") {
        // Check Akamai fingerprint
        if let Some(akamai) = h2_fp.get("akamai_fingerprint") {
            let akamai_str = akamai.as_str().unwrap();
            let parts: Vec<&str> = akamai_str.split('|').collect();

            assert_eq!(parts.len(), 4, "Akamai fingerprint should have 4 parts");

            // Firefox only sends 3 settings (1, 4, 5)
            let settings_parts: Vec<&str> = parts[0]
                .split(';')
                .filter(|s| s.starts_with("1:") || s.starts_with("4:") || s.starts_with("5:"))
                .collect();
            let normalized_settings = settings_parts.join(";");
            assert_eq!(
                normalized_settings, FIREFOX_AKAMAI_SETTINGS,
                "SETTINGS should match Firefox (only 3 settings: 1, 4, 5)"
            );

            assert_eq!(
                parts[1], FIREFOX_WINDOW_UPDATE,
                "WINDOW_UPDATE should match Firefox"
            );
            assert_eq!(
                parts[3], FIREFOX_PSEUDO_ORDER,
                "Pseudo-header order should match Firefox (m,p,a,s)"
            );
        } else {
            panic!("Response should include akamai_fingerprint");
        }

        // Validate sent frames
        if let Some(frames) = h2_fp.get("sent_frames").and_then(|f| f.as_array()) {
            // Frame 0: SETTINGS with only 3 parameters (Firefox doesn't send 2, 3, 6)
            if let Some(settings_frame) = frames.first() {
                assert_eq!(settings_frame["frame_type"], "SETTINGS");
                let settings_list = settings_frame["settings"].as_array().unwrap();

                // Firefox sends exactly 3 settings (no GREASE)
                assert_eq!(
                    settings_list.len(),
                    3,
                    "Firefox should send exactly 3 SETTINGS parameters"
                );

                // Verify settings order and values
                assert_eq!(settings_list[0], "HEADER_TABLE_SIZE = 65536");
                assert_eq!(settings_list[1], "INITIAL_WINDOW_SIZE = 131072");
                assert_eq!(settings_list[2], "MAX_FRAME_SIZE = 16384");
            }

            // Frame 1: WINDOW_UPDATE
            if let Some(wu_frame) = frames.get(1) {
                assert_eq!(wu_frame["frame_type"], "WINDOW_UPDATE");
                assert_eq!(wu_frame["increment"], 12517377);
            }

            // HEADERS frame with Firefox pseudo-order (m,p,a,s)
            // Note: With PRIORITY frames, HEADERS is no longer at index 3
            let headers_frame = frames
                .iter()
                .find(|f| f.get("frame_type").and_then(|v| v.as_str()) == Some("HEADERS"));
            if let Some(headers_frame) = headers_frame {
                let headers = headers_frame["headers"].as_array().unwrap();

                // Verify Firefox pseudo-header order: m,p,a,s
                assert_eq!(headers[0], ":method: GET");
                assert_eq!(headers[1], ":path: /api/all");
                assert_eq!(headers[2], ":authority: tls.peet.ws");
                assert_eq!(headers[3], ":scheme: https");
            }
        }
    }

    // Validate TLS fingerprint (Firefox should NOT use GREASE)
    if let Some(tls_fp) = json.get("tls") {
        if let Some(ja3) = tls_fp.get("ja3_hash") {
            let ja3_str = ja3.as_str().unwrap();

            assert_ne!(
                ja3_str, KNOWN_JA3_PYTHON_REQUESTS,
                "JA3 should NOT match Python requests automation tool fingerprint"
            );
            assert_ne!(
                ja3_str, KNOWN_JA3_CURL_7X,
                "JA3 should NOT match cURL 7.x automation tool fingerprint"
            );
        }
    }
}

#[tokio::test]
async fn test_firefox_browserleaks_passes() {
    let fp = TlsFingerprint::firefox_133();
    let connector = BoringConnector::with_fingerprint(fp);
    let settings = Http2Settings::firefox();
    let uri: Uri = "https://tls.browserleaks.com/json".parse().unwrap();

    let stream = connector
        .connect(&uri)
        .await
        .expect("TLS connection should succeed");

    if !stream.is_h2() {
        warn!("WARNING: Server did not negotiate HTTP/2, skipping test");
        return;
    }

    let mut h2_conn = H2Connection::connect(stream, settings, PseudoHeaderOrder::Firefox)
        .await
        .expect("HTTP/2 connection should succeed");

    let headers = vec![
        (
            "user-agent".to_string(),
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0"
                .to_string(),
        ),
        ("accept".to_string(), "application/json".to_string()),
        ("accept-language".to_string(), "en-US,en;q=0.5".to_string()),
    ];

    let response = h2_conn
        .send_request(Method::GET, &uri, headers, None)
        .await
        .expect("browserleaks.com should accept Firefox fingerprint");

    assert_eq!(
        response.status().as_u16(),
        200,
        "browserleaks.com should return 200 OK"
    );

    // Parse and validate response
    let body = String::from_utf8_lossy(response.body());
    let json: serde_json::Value =
        serde_json::from_str(&body).expect("Response should be valid JSON");

    // Verify JA3 doesn't match automation tool fingerprints
    if let Some(ja3) = json.get("ja3_hash") {
        let ja3_str = ja3.as_str().unwrap();
        assert_ne!(ja3_str, KNOWN_JA3_PYTHON_REQUESTS);
        assert_ne!(ja3_str, KNOWN_JA3_CURL_7X);
    }
}

#[tokio::test]
async fn test_priority_frames_in_akamai() {
    // Test that PRIORITY frames are sent and appear in Akamai fingerprint
    let fp = TlsFingerprint::chrome_142();
    let connector = BoringConnector::with_fingerprint(fp);
    let settings = Http2Settings::default();
    let uri: Uri = "https://tls.peet.ws/api/all".parse().unwrap();

    let stream = connector
        .connect(&uri)
        .await
        .expect("TLS connection should succeed");

    if !stream.is_h2() {
        panic!("Server did not negotiate HTTP/2");
    }

    let mut h2_conn = H2Connection::connect(stream, settings, PseudoHeaderOrder::Chrome)
        .await
        .expect("HTTP/2 connection should succeed");

    let headers = vec![
        (
            "user-agent".to_string(),
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36".to_string(),
        ),
        ("accept".to_string(), "application/json".to_string()),
    ];

    let response = h2_conn
        .send_request(Method::GET, &uri, headers, None)
        .await
        .expect("HTTP/2 request should succeed");

    assert_eq!(response.status().as_u16(), 200);

    // Parse response JSON
    let body = String::from_utf8_lossy(response.body());
    let json: serde_json::Value =
        serde_json::from_str(&body).expect("Response should be valid JSON");

    // Check Akamai fingerprint includes PRIORITY field
    if let Some(h2_fp) = json.get("http2") {
        if let Some(akamai) = h2_fp.get("akamai_fingerprint") {
            let akamai_str = akamai.as_str().unwrap();
            let parts: Vec<&str> = akamai_str.split('|').collect();

            assert_eq!(parts.len(), 4, "Akamai fingerprint should have 4 parts");

            // Part 2 is PRIORITY (format: stream:exclusive:dependency:weight)
            let priority_str = parts[2];

            // Chrome sends PRIORITY frames, so this should not be empty
            // Format may be "0" if no priority frames detected, or comma-separated list
            // The exact format depends on how tls.peet.ws detects and reports PRIORITY frames
            assert!(
                !priority_str.is_empty(),
                "PRIORITY field should be present in Akamai fingerprint"
            );

            // If PRIORITY frames are detected, verify format
            if priority_str != "0" && priority_str.contains(',') {
                // Should contain Chrome priority pattern: 3:0:0:201,5:0:0:101,etc
                assert!(
                    priority_str.contains("3:0:0:201") || priority_str.contains("3"),
                    "PRIORITY should include stream 3"
                );
            }
        }

        // Validate sent frames include PRIORITY frames
        if let Some(frames) = h2_fp.get("sent_frames").and_then(|f| f.as_array()) {
            // Chrome sends PRIORITY frames after SETTINGS and WINDOW_UPDATE
            // They should appear in frames array (typically frames 2-6 for streams 3,5,7,9,11)
            let priority_frames: Vec<&serde_json::Value> = frames
                .iter()
                .filter(|f| f.get("frame_type").and_then(|v| v.as_str()) == Some("PRIORITY"))
                .collect();

            // Chrome should send 5 PRIORITY frames
            assert!(
                priority_frames.len() >= 5,
                "Chrome should send at least 5 PRIORITY frames (streams 3,5,7,9,11)"
            );
        }
    }
}
