use super::*;
use std::net::SocketAddr;
use std::net::TcpListener as StdTcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
use tokio::net::TcpListener;
use tokio::time::{Duration, Instant, timeout};

fn closed_local_port() -> u16 {
    let listener = StdTcpListener::bind("127.0.0.1:0").unwrap();
    let port = listener.local_addr().unwrap().port();
    drop(listener);
    port
}

#[tokio::test]
async fn self_target_detection_matches_literal_ipv4_listener() {
    let local: SocketAddr = "198.51.100.40:443".parse().unwrap();
    assert!(is_mask_target_local_listener_async("198.51.100.40", 443, local, &[],).await);
}

#[tokio::test]
async fn self_target_detection_matches_bracketed_ipv6_listener() {
    let local: SocketAddr = "[2001:db8::44]:8443".parse().unwrap();
    assert!(is_mask_target_local_listener_async("[2001:db8::44]", 8443, local, &[],).await);
}

#[tokio::test]
async fn self_target_detection_keeps_same_ip_different_port_forwardable() {
    let local: SocketAddr = "203.0.113.44:443".parse().unwrap();
    assert!(!is_mask_target_local_listener_async("203.0.113.44", 8443, local, &[],).await);
}

#[tokio::test]
async fn self_target_detection_normalizes_ipv4_mapped_ipv6_literal() {
    let local: SocketAddr = "127.0.0.1:443".parse().unwrap();
    assert!(is_mask_target_local_listener_async("::ffff:127.0.0.1", 443, local, &[],).await);
}

#[tokio::test]
async fn self_target_detection_unspecified_bind_blocks_loopback_target() {
    let local: SocketAddr = "0.0.0.0:443".parse().unwrap();
    assert!(is_mask_target_local_listener_async("127.0.0.1", 443, local, &[],).await);
}

#[tokio::test]
async fn self_target_detection_unspecified_bind_keeps_remote_target_forwardable() {
    let local: SocketAddr = "0.0.0.0:443".parse().unwrap();
    let remote: SocketAddr = "198.51.100.44:443".parse().unwrap();
    assert!(!is_mask_target_local_listener_async("mask.example", 443, local, &[remote],).await);
}

#[tokio::test]
async fn self_target_detection_checks_all_resolved_addresses() {
    let local: SocketAddr = "127.0.0.1:443".parse().unwrap();
    let remote: SocketAddr = "198.51.100.44:443".parse().unwrap();
    let loopback: SocketAddr = "127.0.0.1:443".parse().unwrap();

    assert!(
        is_mask_target_local_listener_async("mask.example", 443, local, &[remote, loopback],).await
    );
}

#[tokio::test]
async fn self_target_fallback_refuses_recursive_loopback_connect() {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let local_addr = listener.local_addr().unwrap();
    let accept_task = tokio::spawn(async move {
        timeout(Duration::from_millis(120), listener.accept())
            .await
            .is_ok()
    });

    let mut config = ProxyConfig::default();
    config.general.beobachten = false;
    config.censorship.mask = true;
    config.censorship.mask_unix_sock = None;
    config.censorship.mask_host = Some(local_addr.ip().to_string());
    config.censorship.mask_port = local_addr.port();
    config.censorship.mask_proxy_protocol = 0;

    let peer: SocketAddr = "203.0.113.90:55090".parse().unwrap();
    let beobachten = BeobachtenStore::new();

    handle_bad_client(
        tokio::io::empty(),
        tokio::io::sink(),
        b"GET /",
        peer,
        local_addr,
        &config,
        &beobachten,
    )
    .await;

    let accepted = accept_task.await.unwrap();
    assert!(
        !accepted,
        "self-target masking must fail closed without connecting to local listener"
    );
}

#[tokio::test]
async fn same_ip_different_port_still_forwards_to_mask_backend() {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let backend_addr = listener.local_addr().unwrap();
    let probe = b"GET /".to_vec();
    let accept_task = tokio::spawn({
        let expected = probe.clone();
        async move {
            let (mut stream, _) = listener.accept().await.unwrap();
            let mut got = vec![0u8; expected.len()];
            stream.read_exact(&mut got).await.unwrap();
            assert_eq!(got, expected);
        }
    });

    let mut config = ProxyConfig::default();
    config.general.beobachten = false;
    config.censorship.mask = true;
    config.censorship.mask_unix_sock = None;
    config.censorship.mask_host = Some("127.0.0.1".to_string());
    config.censorship.mask_port = backend_addr.port();
    config.censorship.mask_proxy_protocol = 0;

    let peer: SocketAddr = "203.0.113.91:55091".parse().unwrap();
    let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
    let beobachten = BeobachtenStore::new();

    handle_bad_client(
        tokio::io::empty(),
        tokio::io::sink(),
        &probe,
        peer,
        local_addr,
        &config,
        &beobachten,
    )
    .await;

    timeout(Duration::from_secs(2), accept_task)
        .await
        .unwrap()
        .unwrap();
}

#[test]
fn detect_client_type_http_boundary_get_and_post() {
    assert_eq!(detect_client_type(b"GET "), "HTTP");
    assert_eq!(detect_client_type(b"GET /"), "HTTP");

    assert_eq!(detect_client_type(b"POST"), "HTTP");
    assert_eq!(detect_client_type(b"POST "), "HTTP");
    assert_eq!(detect_client_type(b"POSTX"), "HTTP");
}

#[test]
fn detect_client_type_tls_and_length_boundaries() {
    assert_eq!(detect_client_type(b"\x16\x03\x01"), "port-scanner");
    assert_eq!(detect_client_type(b"\x16\x03\x01\x00"), "TLS-scanner");

    assert_eq!(detect_client_type(b"123456789"), "port-scanner");
    assert_eq!(detect_client_type(b"1234567890"), "unknown");
}

#[test]
fn build_mask_proxy_header_v1_cross_family_falls_back_to_unknown() {
    let peer: SocketAddr = "192.168.1.5:12345".parse().unwrap();
    let local: SocketAddr = "[2001:db8::1]:443".parse().unwrap();
    let header = build_mask_proxy_header(1, peer, local).unwrap();
    assert_eq!(header, b"PROXY UNKNOWN\r\n");
}

#[test]
fn next_mask_shape_bucket_checked_mul_overflow_fails_closed() {
    let floor = usize::MAX / 2 + 1;
    let cap = usize::MAX;
    let total = floor + 1;
    assert_eq!(next_mask_shape_bucket(total, floor, cap), total);
}

#[tokio::test]
async fn self_target_reject_path_keeps_timing_budget() {
    let mut config = ProxyConfig::default();
    config.general.beobachten = false;
    config.censorship.mask = true;
    config.censorship.mask_unix_sock = None;
    config.censorship.mask_host = Some("127.0.0.1".to_string());
    config.censorship.mask_port = 443;

    let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
    let peer: SocketAddr = "203.0.113.92:55092".parse().unwrap();
    let beobachten = BeobachtenStore::new();

    let (client, server) = duplex(1024);
    drop(client);

    let started = Instant::now();
    handle_bad_client(
        server,
        tokio::io::sink(),
        b"GET / HTTP/1.1\r\n",
        peer,
        local_addr,
        &config,
        &beobachten,
    )
    .await;

    let elapsed = started.elapsed();
    assert!(
        elapsed >= Duration::from_millis(40) && elapsed < Duration::from_millis(250),
        "self-target reject path must keep coarse timing budget without stalling"
    );
}

#[tokio::test]
async fn relay_path_idle_timeout_eviction_remains_effective() {
    let (client_read, mut client_write) = duplex(1024);
    let (mask_read, mask_write) = duplex(1024);

    tokio::spawn(async move {
        tokio::time::sleep(Duration::from_millis(10)).await;
        client_write.write_all(b"a").await.unwrap();
        tokio::time::sleep(Duration::from_millis(180)).await;
        let _ = client_write.write_all(b"b").await;
    });

    let started = Instant::now();
    relay_to_mask(
        client_read,
        tokio::io::sink(),
        mask_read,
        mask_write,
        b"init",
        false,
        0,
        0,
        false,
        0,
        false,
        5 * 1024 * 1024,
        MASK_RELAY_IDLE_TIMEOUT,
    )
    .await;

    let elapsed = started.elapsed();
    assert!(
        elapsed >= Duration::from_millis(90) && elapsed < Duration::from_millis(180),
        "idle-timeout eviction must occur before late trickle write"
    );
}

#[tokio::test]
async fn offline_mask_target_refusal_respects_timing_normalization_budget() {
    let mut config = ProxyConfig::default();
    config.general.beobachten = false;
    config.censorship.mask = true;
    config.censorship.mask_unix_sock = None;
    config.censorship.mask_host = Some("127.0.0.1".to_string());
    config.censorship.mask_port = closed_local_port();
    config.censorship.mask_timing_normalization_enabled = true;
    config.censorship.mask_timing_normalization_floor_ms = 120;
    config.censorship.mask_timing_normalization_ceiling_ms = 120;

    let peer: SocketAddr = "203.0.113.93:55093".parse().unwrap();
    let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
    let beobachten = BeobachtenStore::new();

    let (mut client, server) = duplex(1024);
    let started = Instant::now();
    let task = tokio::spawn(async move {
        handle_bad_client(
            server,
            tokio::io::sink(),
            b"GET / HTTP/1.1\r\n\r\n",
            peer,
            local_addr,
            &config,
            &beobachten,
        )
        .await;
    });

    client.shutdown().await.unwrap();
    timeout(Duration::from_secs(2), task)
        .await
        .unwrap()
        .unwrap();
    let elapsed = started.elapsed();

    assert!(
        elapsed >= Duration::from_millis(110) && elapsed < Duration::from_millis(220),
        "offline-refusal path must honor normalization budget without unbounded drift"
    );
}

#[tokio::test]
async fn offline_mask_target_refusal_with_idle_client_is_bounded_by_consume_timeout() {
    let mut config = ProxyConfig::default();
    config.general.beobachten = false;
    config.censorship.mask = true;
    config.censorship.mask_unix_sock = None;
    config.censorship.mask_host = Some("127.0.0.1".to_string());
    config.censorship.mask_port = closed_local_port();
    config.censorship.mask_timing_normalization_enabled = false;

    let peer: SocketAddr = "203.0.113.94:55094".parse().unwrap();
    let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
    let beobachten = BeobachtenStore::new();

    let (mut client, server) = duplex(1024);
    let started = Instant::now();
    let task = tokio::spawn(async move {
        handle_bad_client(
            server,
            tokio::io::sink(),
            b"GET / HTTP/1.1\r\n\r\n",
            peer,
            local_addr,
            &config,
            &beobachten,
        )
        .await;
    });

    tokio::time::sleep(Duration::from_millis(120)).await;
    client
        .write_all(b"still-open-before-timeout")
        .await
        .expect("connection should still be open before consume timeout expires");

    timeout(Duration::from_secs(2), task)
        .await
        .unwrap()
        .unwrap();
    let elapsed = started.elapsed();

    assert!(
        elapsed >= Duration::from_millis(190) && elapsed < Duration::from_millis(350),
        "offline-refusal path must not retain idle client indefinitely"
    );
}
