//! Tests for configuration edge cases and validation.

use std::net::Ipv4Addr;
use std::time::{Duration, Instant};

use str0m::media::{Direction, MediaKind};
use str0m::{Event, RtcConfig, RtcError};
use tracing::info_span;

mod common;
use common::{Peer, TestRtc, init_crypto_default, init_log, negotiate, progress};

/// Test set_reordering_size_audio() and set_reordering_size_video() with custom sizes.
#[test]
fn config_reordering_size_custom() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    // Verify config builder correctly sets and retrieves values
    let config = RtcConfig::new()
        .set_reordering_size_audio(20)
        .set_reordering_size_video(50);

    // Verify the config has correct values before building
    assert_eq!(
        config.reordering_size_audio(),
        20,
        "Audio reordering size should be 20"
    );
    assert_eq!(
        config.reordering_size_video(),
        50,
        "Video reordering size should be 50"
    );

    // Verify default values are different
    let default_config = RtcConfig::new();
    assert_eq!(
        default_config.reordering_size_audio(),
        15,
        "Default audio reordering should be 15"
    );
    assert_eq!(
        default_config.reordering_size_video(),
        30,
        "Default video reordering should be 30"
    );

    // Build and verify the Rtc works with custom config
    let rtc = config.build(Instant::now());
    let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc);
    let mut r = TestRtc::new(Peer::Right);

    l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
    r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());

    let mid = negotiate(&mut l, &mut r, |change| {
        change.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None)
    });

    loop {
        if l.is_connected() && r.is_connected() {
            break;
        }
        if l.duration() > Duration::from_secs(5) {
            panic!("Failed to connect");
        }
        progress(&mut l, &mut r)?;
    }

    // Send and receive data to verify the config doesn't break functionality
    let params = l.params_opus();
    let pt = params.pt();
    let data = vec![1_u8; 80];

    for _ in 0..20 {
        let wallclock = l.start + l.duration();
        let time = l.duration().into();
        l.writer(mid)
            .unwrap()
            .write(pt, wallclock, time, data.clone())?;
        progress(&mut l, &mut r)?;
    }

    // Verify data was received
    let received_count = r
        .events
        .iter()
        .filter(|(_, e)| matches!(e, Event::MediaData(_)))
        .count();

    assert!(
        received_count > 10,
        "Should receive media data with custom reordering config, got {}",
        received_count
    );

    Ok(())
}

/// Test enable_raw_packets(true) produces RawPacket events.
#[test]
fn config_raw_packets_enabled() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    let rtc = RtcConfig::new()
        .enable_raw_packets(true)
        .build(Instant::now());

    let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc);

    let rtc_r = RtcConfig::new()
        .enable_raw_packets(true)
        .build(Instant::now());
    let mut r = TestRtc::new_with_rtc(info_span!("R"), rtc_r);

    l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
    r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());

    let mid = negotiate(&mut l, &mut r, |change| {
        change.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None)
    });

    loop {
        if l.is_connected() && r.is_connected() {
            break;
        }
        if l.duration() > Duration::from_secs(5) {
            panic!("Failed to connect");
        }
        progress(&mut l, &mut r)?;
    }

    // Clear previous events
    l.events.clear();
    r.events.clear();

    // Send some audio data
    let params = l.params_opus();
    let pt = params.pt();
    let data = vec![1_u8; 80];

    for _ in 0..20 {
        let wallclock = l.start + l.duration();
        let time = l.duration().into();
        l.writer(mid)
            .unwrap()
            .write(pt, wallclock, time, data.clone())?;
        progress(&mut l, &mut r)?;
    }

    // Check for RawPacket events
    let raw_packet_count = r
        .events
        .iter()
        .filter(|(_, e)| matches!(e, Event::RawPacket(_)))
        .count();

    assert!(
        raw_packet_count > 0,
        "Should have RawPacket events with enable_raw_packets(true)"
    );

    Ok(())
}

/// Test set_stats_interval with custom duration.
#[test]
fn config_stats_interval_custom() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    let stats_interval = Duration::from_secs(1);
    let rtc = RtcConfig::new()
        .set_stats_interval(Some(stats_interval))
        .build(Instant::now());

    let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc);

    let rtc_r = RtcConfig::new()
        .set_stats_interval(Some(stats_interval))
        .build(Instant::now());
    let mut r = TestRtc::new_with_rtc(info_span!("R"), rtc_r);

    l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
    r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());

    let mid = negotiate(&mut l, &mut r, |change| {
        change.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None)
    });

    loop {
        if l.is_connected() && r.is_connected() {
            break;
        }
        progress(&mut l, &mut r)?;
    }

    let max = l.last.max(r.last);
    l.last = max;
    r.last = max;

    let params = l.params_opus();
    let pt = params.pt();
    let data = vec![1_u8; 80];

    l.set_forced_time_advance(Duration::from_millis(1));
    r.set_forced_time_advance(Duration::from_millis(1));

    // Run for 5 seconds to get multiple stats events
    loop {
        let wallclock = l.start + l.duration();
        let time = l.duration().into();
        l.writer(mid)
            .unwrap()
            .write(pt, wallclock, time, data.clone())?;
        progress(&mut l, &mut r)?;
        if l.duration() > Duration::from_secs(5) {
            break;
        }
    }

    // Should have multiple stats events with 1s interval over 5s
    let peer_stats_count = l
        .events
        .iter()
        .filter(|(_, e)| matches!(e, Event::PeerStats(_)))
        .count();

    assert!(
        peer_stats_count >= 3,
        "Expected at least 3 PeerStats events with 1s interval over 5s, got {}",
        peer_stats_count
    );

    Ok(())
}

/// Test set_stats_interval(None) produces no stats events.
#[test]
fn config_stats_disabled() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    let rtc = RtcConfig::new()
        .set_stats_interval(None)
        .build(Instant::now());

    let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc);

    let rtc_r = RtcConfig::new()
        .set_stats_interval(None)
        .build(Instant::now());
    let mut r = TestRtc::new_with_rtc(info_span!("R"), rtc_r);

    l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
    r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());

    let mid = negotiate(&mut l, &mut r, |change| {
        change.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None)
    });

    loop {
        if l.is_connected() && r.is_connected() {
            break;
        }
        progress(&mut l, &mut r)?;
    }

    let max = l.last.max(r.last);
    l.last = max;
    r.last = max;

    let params = l.params_opus();
    let pt = params.pt();
    let data = vec![1_u8; 80];

    l.set_forced_time_advance(Duration::from_millis(1));
    r.set_forced_time_advance(Duration::from_millis(1));

    loop {
        let wallclock = l.start + l.duration();
        let time = l.duration().into();
        l.writer(mid)
            .unwrap()
            .write(pt, wallclock, time, data.clone())?;
        progress(&mut l, &mut r)?;
        if l.duration() > Duration::from_secs(3) {
            break;
        }
    }

    let peer_stats_count = l
        .events
        .iter()
        .filter(|(_, e)| matches!(e, Event::PeerStats(_)))
        .count();

    assert_eq!(
        peer_stats_count, 0,
        "Should have no PeerStats events when stats disabled"
    );

    Ok(())
}

/// Test set_fingerprint_verification(false) allows connection with wrong fingerprint.
#[test]
fn config_fingerprint_verification_disabled() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    use str0m::Candidate;
    use str0m::crypto::Fingerprint;

    // Create RTCs with fingerprint verification DISABLED
    let rtc_l = RtcConfig::new()
        .set_fingerprint_verification(false)
        .set_rtp_mode(true)
        .build(Instant::now());

    let rtc_r = RtcConfig::new()
        .set_fingerprint_verification(false)
        .set_rtp_mode(true)
        .build(Instant::now());

    let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc_l);
    let mut r = TestRtc::new_with_rtc(info_span!("R"), rtc_r);

    // Set up candidates
    let host1 = Candidate::host((Ipv4Addr::new(1, 1, 1, 1), 1000).into(), "udp").unwrap();
    let host2 = Candidate::host((Ipv4Addr::new(2, 2, 2, 2), 2000).into(), "udp").unwrap();
    l.add_local_candidate(host1.clone()).unwrap();
    l.add_remote_candidate(host2.clone());
    r.add_local_candidate(host2).unwrap();
    r.add_remote_candidate(host1);

    // Create CORRUPTED fingerprints (all zeros - definitely wrong)
    let corrupted_fingerprint = Fingerprint {
        hash_func: "sha-256".to_string(),
        bytes: vec![0u8; 32], // Wrong fingerprint!
    };

    // Set the WRONG fingerprints as remote (this would fail with verification enabled)
    l.direct_api()
        .set_remote_fingerprint(corrupted_fingerprint.clone());
    r.direct_api().set_remote_fingerprint(corrupted_fingerprint);

    // Exchange ICE credentials
    let creds_l = l.direct_api().local_ice_credentials();
    let creds_r = r.direct_api().local_ice_credentials();
    l.direct_api().set_remote_ice_credentials(creds_r);
    r.direct_api().set_remote_ice_credentials(creds_l);

    l.direct_api().set_ice_controlling(true);
    r.direct_api().set_ice_controlling(false);

    // Start DTLS - this should succeed despite wrong fingerprints
    // because verification is disabled
    l.direct_api().start_dtls(true).unwrap();
    r.direct_api().start_dtls(false).unwrap();

    l.direct_api().start_sctp(true);
    r.direct_api().start_sctp(false);

    // Connection should succeed despite corrupted fingerprints
    loop {
        if l.is_connected() && r.is_connected() {
            break;
        }
        if l.duration() > Duration::from_secs(5) {
            panic!("Failed to connect - fingerprint verification should be disabled");
        }
        progress(&mut l, &mut r)?;
    }

    // Verify we actually connected
    assert!(l.is_connected(), "L should be connected");
    assert!(r.is_connected(), "R should be connected");

    Ok(())
}

/// Test that RtcConfig can be cloned and used for multiple instances.
#[test]
fn config_clone_multiple_instances() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    // Create a config with custom settings (not ice_lite since both can't be ice_lite)
    let config = RtcConfig::new()
        .set_reordering_size_audio(20)
        .set_reordering_size_video(40);

    // Clone the config and create multiple instances
    let rtc1 = config.clone().build(Instant::now());
    let rtc2 = config.build(Instant::now());

    let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc1);
    let mut r = TestRtc::new_with_rtc(info_span!("R"), rtc2);

    l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
    r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());

    let (offer, pending) = l.span.in_scope(|| {
        let mut change = l.rtc.sdp_api();
        let _ = change.add_channel("test".into());
        change.apply().unwrap()
    });

    let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?;
    l.span
        .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?;

    loop {
        if l.is_connected() && r.is_connected() {
            break;
        }
        if l.duration() > Duration::from_secs(5) {
            panic!("Failed to connect with cloned config");
        }
        progress(&mut l, &mut r)?;
    }

    Ok(())
}

/// Test set_send_buffer_audio and set_send_buffer_video configuration.
#[test]
fn config_send_buffer_sizes() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    // Verify config builder correctly sets and retrieves values
    let config = RtcConfig::new()
        .set_send_buffer_audio(100)
        .set_send_buffer_video(2000);

    // Verify the config has correct values before building
    assert_eq!(
        config.send_buffer_audio(),
        100,
        "Audio send buffer should be 100"
    );
    assert_eq!(
        config.send_buffer_video(),
        2000,
        "Video send buffer should be 2000"
    );

    // Verify default values are different
    let default_config = RtcConfig::new();
    assert_eq!(
        default_config.send_buffer_audio(),
        50,
        "Default audio send buffer should be 50"
    );
    assert_eq!(
        default_config.send_buffer_video(),
        1000,
        "Default video send buffer should be 1000"
    );

    // Build and verify the Rtc works with custom config
    let rtc = config.build(Instant::now());
    let mut l = TestRtc::new_with_rtc(info_span!("L"), rtc);
    let mut r = TestRtc::new(Peer::Right);

    l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
    r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());

    let mid = negotiate(&mut l, &mut r, |change| {
        change.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None)
    });

    loop {
        if l.is_connected() && r.is_connected() {
            break;
        }
        if l.duration() > Duration::from_secs(5) {
            panic!("Failed to connect");
        }
        progress(&mut l, &mut r)?;
    }

    // Send and receive data to verify the config doesn't break functionality
    let params = l.params_opus();
    let pt = params.pt();
    let data = vec![1_u8; 80];

    for _ in 0..20 {
        let wallclock = l.start + l.duration();
        let time = l.duration().into();
        l.writer(mid)
            .unwrap()
            .write(pt, wallclock, time, data.clone())?;
        progress(&mut l, &mut r)?;
    }

    // Verify data was received
    let received_count = r
        .events
        .iter()
        .filter(|(_, e)| matches!(e, Event::MediaData(_)))
        .count();

    assert!(
        received_count > 10,
        "Should receive media data with custom send buffer config, got {}",
        received_count
    );

    Ok(())
}
