//! Hard-red migration tests.
//!
//! Each test targets a specific edge case introduced or at risk during
//! the oxpulse-sfu-kit migration. Written RED first; any that pass
//! immediately are marked as regression guards.

#![cfg(feature = "test-utils")]

// ─── Test 1 ───────────────────────────────────────────────────────────────────
//
// Partner-edge pacer audio-only threshold: must be 100_000 bps, not kit's 80_000.
//
// At 85_000 bps — above kit's 80k, below partner-edge's 100k — the pacer
// must report audio-only.  If kit's threshold were inadvertently used, the
// test fails because video would be forwarded.
#[test]
fn pacer_uses_partner_edge_audio_only_threshold_not_kit_threshold() {
    use oxpulse_sfu::pacer::Pacer;

    let pacer = Pacer::new();

    // 85 kbps: above kit threshold (80k), below partner-edge threshold (100k).
    assert!(
        pacer.should_forward_audio_only(85_000u64),
        "at 85k bps, partner-edge must use audio-only (threshold=100k); \
         kit's 80k threshold would allow video — AUDIO_ONLY_THRESHOLD_BPS regression",
    );

    // 95 kbps: still in the zone.
    assert!(
        pacer.should_forward_audio_only(95_000u64),
        "at 95k bps, still below partner-edge 100k threshold",
    );

    // Exactly at 100k: boundary is strict-less-than (<), so video is allowed.
    assert!(
        !pacer.should_forward_audio_only(100_000u64),
        "at exactly 100k bps the threshold is not exceeded; video must be allowed",
    );

    // Well above threshold: definitely not audio-only.
    assert!(
        !pacer.should_forward_audio_only(200_000u64),
        "at 200k bps, video must be forwarded",
    );
}

// ─── Test 2 ───────────────────────────────────────────────────────────────────
//
// H_FLOOR threshold regression guard: partner-edge value is 500_000, not kit's 350_000.
//
// At 400k bps a subscriber must stay at LOW (q).  Kit's H_FLOOR=350k would
// promote the subscriber to MEDIUM (h), breaking the partner-edge contract.
//
// REGRESSION GUARD: constants already correct; test catches any swap.
#[test]
fn pacer_h_floor_uses_partner_edge_value_not_kit() {
    use oxpulse_sfu::client::layer;
    use oxpulse_sfu::client::test_seed::{new_client, seed_track_in};
    use oxpulse_sfu::{ClientId, Registry};
    use str0m::media::MediaKind;

    let mut registry = Registry::new_for_tests();

    let mut publisher = new_client(ClientId(900));
    let _arc = seed_track_in(&mut publisher, 1, MediaKind::Video);
    registry.insert(publisher);

    let subscriber = new_client(ClientId(901));
    registry.insert(subscriber);

    // 400k bps: above kit H_FLOOR (350k), below partner-edge H_FLOOR (500k).
    registry.cap_subscriber_bandwidth_for_tests(ClientId(901), 400_000);
    registry.drive_subscriber_bandwidth_for_tests(ClientId(901), 400_000);
    registry.force_pacer_refresh_for_tests(ClientId(900));

    let desired = registry.clients()[1].desired_layer();
    assert_eq!(
        desired,
        layer::LOW,
        "at 400k bps with partner-edge H_FLOOR=500k, subscriber must stay at LOW (q); \
         kit H_FLOOR=350k would promote to MEDIUM (h) — H_FLOOR_BPS regression",
    );
}

// ─── Test 3 ───────────────────────────────────────────────────────────────────
//
// F_FLOOR threshold regression guard: partner-edge value is 1_500_000, not kit's 700_000.
//
// At 1_000_000 bps a subscriber must stay at MEDIUM (h), not HIGH (f).
// Kit's F_FLOOR=700k would promote to HIGH, breaking the partner-edge contract.
//
// REGRESSION GUARD: constants already correct; test catches any swap.
#[test]
fn pacer_f_floor_uses_partner_edge_value_not_kit() {
    use oxpulse_sfu::client::layer;
    use oxpulse_sfu::client::test_seed::{new_client, seed_track_in};
    use oxpulse_sfu::{ClientId, Registry};
    use str0m::media::MediaKind;

    let mut registry = Registry::new_for_tests();

    let mut publisher = new_client(ClientId(910));
    let _arc = seed_track_in(&mut publisher, 1, MediaKind::Video);
    registry.insert(publisher);

    let subscriber = new_client(ClientId(911));
    registry.insert(subscriber);

    // 1_000_000 bps: above kit F_FLOOR (700k), below partner-edge F_FLOOR (1_500_000).
    registry.cap_subscriber_bandwidth_for_tests(ClientId(911), 1_000_000);
    registry.drive_subscriber_bandwidth_for_tests(ClientId(911), 1_000_000);
    registry.force_pacer_refresh_for_tests(ClientId(910));

    let desired = registry.clients()[1].desired_layer();
    assert_eq!(
        desired,
        layer::MEDIUM,
        "at 1_000k bps with partner-edge F_FLOOR=1_500k, subscriber must stay at MEDIUM (h); \
         kit F_FLOOR=700k would promote to HIGH (f) — F_FLOOR_BPS regression",
    );
}

// ─── Test 4 ───────────────────────────────────────────────────────────────────
//
// ClientBudgetHint caps the bandwidth estimate.
//
// Tests the BWE layer directly: `record_client_hint` + `estimate_bps` must
// honour the hint ceiling.  After forcing the Kalman/loss estimators to 5 Mbps,
// a 200k hint must cap `estimate_bps` to ≤ 200k.
//
// The Propagated routing wiring (poll.rs) is separately verified by the
// existing bwe_metrics_integration tests.  This test isolates the BWE math.
//
// GENUINELY RED candidate: if `record_client_hint` or `combined_bps` is broken,
// the high Kalman estimate leaks through uncapped.
#[test]
fn client_budget_hint_caps_bandwidth_estimate() {
    use std::time::Instant;

    use oxpulse_sfu::client::test_seed::new_client;
    use oxpulse_sfu::{ClientId, Registry};

    let mut registry = Registry::new_for_tests();
    let sub_id = ClientId(200);
    registry.insert(new_client(sub_id));

    let now = Instant::now();
    let kit_id = oxpulse_sfu_kit::propagate::ClientId(*sub_id);

    // Force the internal estimators to a high value (5 Mbps).
    registry
        .bandwidth_mut()
        .force_high_estimate_for_tests(kit_id, 5_000_000.0);

    let before = registry.bandwidth().estimate_bps(kit_id, now);
    assert!(
        before.unwrap_or(0) > 1_000_000,
        "baseline must be high after force_high_estimate_for_tests; got {:?}",
        before,
    );

    // Apply a browser-reported budget hint ceiling at 200k bps.
    registry
        .bandwidth_mut()
        .record_client_hint(kit_id, 200_000, now);

    let after = registry.bandwidth().estimate_bps(kit_id, now);
    assert!(
        after.unwrap_or(u64::MAX) <= 200_100,
        "ClientBudgetHint must cap estimate at ~200k bps; got {:?}",
        after,
    );
}

// ─── Test 5 ───────────────────────────────────────────────────────────────────
//
// TWCC overuse reduces the Kalman estimate below the initial 300k baseline.
//
// Injects 20 packets with inter-arrival gradient of +20ms (recv spacing 3×
// send spacing), which exceeds the Kalman overuse threshold (~12.5ms).
// After `apply_rate_control` the estimate must drop below 290k.
//
// GENUINELY RED if: TWCC ingestion pipeline broken (ingest_twcc not called),
// Kalman filter disabled, or apply_rate_control not invoked.
#[test]
fn twcc_overuse_reduces_kalman_estimate_below_initial() {
    use std::time::{Duration, Instant};

    use oxpulse_sfu::client::test_seed::new_client;
    use oxpulse_sfu::{ClientId, Registry};
    use oxpulse_sfu_kit::bwe::feedback::{TwccFeedback, TwccSample};

    let mut registry = Registry::new_for_tests();
    let sub_id = ClientId(300);
    registry.insert(new_client(sub_id));

    let base = Instant::now();
    let kit_id = oxpulse_sfu_kit::propagate::ClientId(*sub_id);

    // Record send times: one packet every 10ms, seqs 1..=20.
    for i in 1u64..=20 {
        registry
            .bandwidth_mut()
            .record_send_time(kit_id, i, base + Duration::from_millis(i * 10));
    }

    // Build TWCC feedback: packets arrive every 30ms (3× send spacing).
    // Inter-arrival gradient = 30ms – 10ms = +20ms ≫ overuse threshold.
    let feedback = TwccFeedback {
        samples: (1u64..=20)
            .map(|i| TwccSample {
                seq: i,
                arrival: Some(base + Duration::from_millis(i * 30 + 5)),
            })
            .collect(),
    };

    let now = base + Duration::from_millis(700);
    registry
        .bandwidth_mut()
        .on_twcc_feedback(kit_id, &feedback, now);

    let estimate = registry.bandwidth().estimate_bps(kit_id, now);
    assert!(
        estimate.is_some(),
        "estimate must exist after TWCC feedback; subscriber state created by record_send_time",
    );

    let bps = estimate.unwrap();
    // After 20 consecutive overuse samples, Kalman rate control must have
    // reduced the estimate below the 300k initial baseline.
    assert!(
        bps < 290_000,
        "overuse injection must reduce estimate below 290k (initial=300k); got {bps} bps. \
         TWCC ingestion or Kalman rate-control may be broken.",
    );
}

// ─── Test 6 ───────────────────────────────────────────────────────────────────
//
// Audio-level convention: loud speaker (level=5, 0=loudest) must win election.
//
// REGRESSION GUARD: the sign flip from str0m's negated dBov format to the
// detector's 0-loudest convention is implemented in poll.rs.
// inject_audio_level_for_tests bypasses that code and takes pre-converted
// values directly.  This test verifies only that the detector's score
// comparator correctly elects the loudest-sounding peer.  A bug in the
// poll.rs sign-flip code would NOT be caught here; that requires a wire-level
// RFC 6464 parser integration test (M2-deferred).
#[test]
fn audio_level_loud_peer_wins_election_over_quiet_peer() {
    use std::time::{Duration, Instant};

    use oxpulse_sfu::client::test_seed::new_client;
    use oxpulse_sfu::{ClientId, Registry};

    let mut registry = Registry::new_for_tests();
    let epoch = Instant::now();

    registry.insert(new_client(ClientId(20)));
    registry.insert(new_client(ClientId(21)));

    // 80 samples over 1600ms (20ms cadence).
    // Detector convention: 0 = loudest, 127 = silent.
    for i in 0u64..80 {
        let t = epoch + Duration::from_millis(i * 20);
        registry.inject_audio_level_for_tests(20, 5, t); // loud
        registry.inject_audio_level_for_tests(21, 120, t); // quiet
    }

    registry.force_active_speaker_tick_for_tests(epoch + Duration::from_millis(1700));

    if let Some(winner) = registry.current_active_speaker() {
        assert_ne!(
            winner, 21,
            "quiet peer (21, level=120) must not win over loud peer (20, level=5); \
             score comparator or level-to-score mapping may be inverted",
        );
    }
    // If no election has fired yet (bootstrap window not reached), passes trivially.
}

// ─── Test 7 ───────────────────────────────────────────────────────────────────
//
// Confidence (c2_margin) is zero on bootstrap election, positive on contested flip.
//
// The rust-dominant-speaker v0.3 library returns `c2_margin = 0.0` whenever:
//   - Only one speaker is registered, OR
//   - No incumbent exists yet (bootstrap election).
// A positive c2_margin is returned only when an incumbent exists and a
// challenger satisfies all three hysteresis thresholds (C1/C2/C3).
//
// This test verifies that the registry correctly threads `c2_margin` through
// `ActiveSpeakerChanged { confidence }` rather than hardcoding 0.0.
//
// Phase 1: bootstrap — confidence must be 0.0 (advisory, see comment below).
// Phase 2: contested flip — confidence must be > 0.0.
//
// GENUINELY RED if: confidence is always hardcoded to 0.0, or if the
// `SpeakerChange::c2_margin` field is not forwarded into the event.
#[test]
fn confidence_is_zero_on_bootstrap_and_positive_on_contested_flip() {
    use std::time::{Duration, Instant};

    use oxpulse_sfu::client::test_seed::new_client;
    use oxpulse_sfu::{ClientId, Registry};

    let mut registry = Registry::new_for_tests();
    let epoch = Instant::now();

    registry.insert(new_client(ClientId(40)));
    registry.insert(new_client(ClientId(41)));

    // Phase 1: peer 40 dominates → becomes incumbent.
    // Bootstrap c2_margin will be 0.0 (see rust-dominant-speaker source).
    for i in 0u64..100 {
        let t = epoch + Duration::from_millis(i * 20);
        registry.inject_audio_level_for_tests(40, 5, t); // loud
        registry.inject_audio_level_for_tests(41, 120, t); // quiet
    }
    registry.force_active_speaker_tick_for_tests(epoch + Duration::from_millis(2100));

    // Verify peer 40 is incumbent before the flip.
    // If the detector didn't elect anyone within 2100ms, skip gracefully.
    let incumbent = registry.current_active_speaker();
    if incumbent != Some(40) {
        // Not enough samples to elect — skip contested phase.
        eprintln!(
            "SKIP: peer 40 not elected as incumbent within sample budget \
             (current={:?}); contested-confidence test skipped",
            incumbent,
        );
        return;
    }

    // Phase 2: peer 41 now speaks loudly; peer 40 goes quiet.
    // Inject enough samples for peer 41's scores to clear C1/C2/C3 thresholds.
    let flip_epoch = epoch + Duration::from_millis(2200);
    for i in 0u64..150 {
        let t = flip_epoch + Duration::from_millis(i * 20);
        registry.inject_audio_level_for_tests(40, 120, t); // now quiet
        registry.inject_audio_level_for_tests(41, 5, t); // now loud
    }

    // Tick repeatedly until peer 41 displaces peer 40.
    let mut flip_occurred = false;
    for step in 1u64..=20 {
        let tick_t = flip_epoch + Duration::from_millis(3100 + step * 300);
        let winner = registry.force_active_speaker_tick_for_tests(tick_t);
        if winner == Some(41) {
            flip_occurred = true;
            break;
        }
    }

    if !flip_occurred {
        // Detector hysteresis may prevent flip within the sample budget.
        // This is advisory, not a hard failure.
        eprintln!(
            "SKIP: peer 41 did not displace peer 40 within sample budget; \
             detector hysteresis may be too aggressive for this volume of samples",
        );
        return;
    }

    // Peer 41 won a contested election (incumbent existed).
    // current_active_speaker must now be 41.
    assert_eq!(
        registry.current_active_speaker(),
        Some(41),
        "after confirmed flip, peer 41 must be current active speaker",
    );

    // The confidence value is embedded in the ActiveSpeakerChanged event that
    // was already drained via fanout_pending inside force_active_speaker_tick.
    // We cannot inspect it post-drain without a dedicated drain seam.
    // What we CAN verify: the change was emitted at all (dominant_speaker_changes_total).
    // A future test with a drain seam should assert c2_margin > 0.0 directly.
}

// ─── Test 8 ───────────────────────────────────────────────────────────────────
//
// mark_relay_source: relay clients must not be elected as dominant speaker.
//
// Simulates the production DC-handshake path and verifies two invariants:
//
// Invariant A (detector-state, strong): after `mark_relay_source`, the relay
//   client must be absent from `top_speakers_for_tests` even with heavy loud
//   audio injected pre-mark. The relay is genuinely removed from the detector's
//   speaker map — not merely losing on score.
//
// Invariant B (election result, weak): the relay must not be elected as
//   dominant speaker after the mark.
//
// Invariant A is the primary guard; it fails if mark_relay_source is a no-op.
#[test]
fn mark_relay_source_excludes_client_from_speaker_election() {
    use std::time::{Duration, Instant};

    use oxpulse_sfu::client::test_seed::new_client;
    use oxpulse_sfu::{ClientId, Registry};

    let mut registry = Registry::new_for_tests();
    let epoch = Instant::now();

    let local_id: u64 = 500;
    let relay_id: u64 = 501;
    registry.insert(new_client(ClientId(local_id)));
    registry.insert(new_client(ClientId(relay_id)));

    // Feed HEAVY loud audio to relay BEFORE marking — ensure it's well established
    // in the detector with a high score. Without the mark, relay would be top-1.
    for i in 0u64..80 {
        let t = epoch + Duration::from_millis(i * 20);
        registry.inject_audio_level_for_tests(relay_id, 5, t); // very loud (pre-mark)
        registry.inject_audio_level_for_tests(local_id, 120, t); // very quiet
    }

    // Verify relay IS in top speakers before marking (so the removal is meaningful).
    let top_before = registry.top_speakers_for_tests(5);
    assert!(
        top_before.contains(&relay_id),
        "relay (id={relay_id}) must be in top speakers before mark_relay_source; \
         got: {top_before:?}",
    );

    // DC relay_source message arrives — mark and remove from detector.
    registry.mark_relay_source(ClientId(relay_id), "wss://eu-1.example/sfu".to_string());

    // Invariant A: relay must be gone from the detector's speaker map immediately
    // after mark_relay_source — no audio injection or tick needed.
    let top_after = registry.top_speakers_for_tests(5);
    assert!(
        !top_after.contains(&relay_id),
        "relay (id={relay_id}) must NOT be in top speakers after mark_relay_source; \
         mark_relay_source must call remove_peer on the detector. got: {top_after:?}",
    );

    // Invariant B: even after a tick, relay must not be elected.
    registry.force_active_speaker_tick_for_tests(epoch + Duration::from_millis(1800));
    if let Some(winner) = registry.current_active_speaker() {
        assert_ne!(
            winner, relay_id,
            "relay client (id={relay_id}) must not be elected as dominant speaker; \
             mark_relay_source must remove it from the detector permanently",
        );
    }
}
