use naia_socket_shared::Instant;
use crate::connection::{
bandwidth::BandwidthConfig,
bandwidth_accumulator::BandwidthAccumulator,
entity_priority::{EntityPriorityData, EntityPriorityMut},
priority_state::{GlobalPriorityState, UserPriorityState},
};
use crate::messages::channels::channel::ChannelCriticality;
fn init_clock() {
#[cfg(feature = "test_time")]
naia_socket_shared::TestClock::init(0);
}
fn advance(t: &Instant, ms: u32) -> Instant {
let mut out = t.clone();
out.add_millis(ms);
out
}
#[test]
fn a_bdd_1_bandwidth_cap_bounds_tick_bytes() {
init_clock();
let cfg = BandwidthConfig { target_bytes_per_sec: 64_000 };
let mut acc = BandwidthAccumulator::new(&cfg);
let t0 = Instant::now();
acc.accumulate(&t0);
let t1 = advance(&t0, 17);
acc.accumulate(&t1);
const MTU: u32 = 430;
let mut bytes_this_tick: u64 = 0;
let mut packets_this_tick: u32 = 0;
for _ in 0..10_000 {
if !acc.can_spend(MTU) {
break;
}
acc.spend(MTU);
bytes_this_tick += MTU as u64;
packets_this_tick += 1;
}
assert!(
bytes_this_tick <= 1088 + MTU as u64,
"tick sent {} bytes, budget+overshoot=1518",
bytes_this_tick
);
assert!(packets_this_tick < 10, "expected burst gated; got {} packets", packets_this_tick);
}
#[test]
fn a_bdd_2_queue_drains_over_ticks() {
init_clock();
let cfg = BandwidthConfig { target_bytes_per_sec: 64_000 };
let mut acc = BandwidthAccumulator::new(&cfg);
const MTU: u32 = 430;
let mut now = Instant::now();
acc.accumulate(&now);
let mut total_packets: u32 = 0;
for _tick in 0..120 {
now = advance(&now, 17);
acc.accumulate(&now);
for _ in 0..10_000 {
if !acc.can_spend(MTU) {
break;
}
acc.spend(MTU);
total_packets += 1;
}
}
assert!(total_packets > 200, "expected sustained drain; got {} packets", total_packets);
assert!(total_packets < 400, "overshoot-bounded; got {} packets", total_packets);
}
#[test]
fn a_bdd_3_high_outranks_low_at_equal_age() {
let high = ChannelCriticality::High.base_gain();
let low = ChannelCriticality::Low.base_gain();
assert!(high > low, "High({}) must exceed Low({})", high, low);
assert!((high / low).round() as i32 >= 20);
}
#[test]
fn a_bdd_4_default_budget_does_not_defer_light_traffic() {
init_clock();
let cfg = BandwidthConfig::default();
let mut acc = BandwidthAccumulator::new(&cfg);
let mut now = Instant::now();
acc.accumulate(&now);
now = advance(&now, 17);
acc.accumulate(&now);
const MTU: u32 = 430;
assert!(acc.can_spend(MTU), "one MTU packet must fit in default budget");
acc.spend(MTU);
now = advance(&now, 17);
acc.accumulate(&now);
assert!(acc.can_spend(MTU), "second MTU must fit after surplus carry");
}
#[test]
fn a_bdd_7_low_catches_up_to_high_within_bounded_ticks() {
let high_weight_after_1_tick = ChannelCriticality::High.base_gain() * 1.0;
let low_catchup_ticks = (high_weight_after_1_tick / ChannelCriticality::Low.base_gain()).ceil() as u32;
assert_eq!(low_catchup_ticks, 20);
assert!(low_catchup_ticks <= 60, "catch-up bounded within one second of ticks");
}
#[test]
fn b_bdd_1_unsent_entity_accumulator_carries_across_tick() {
let mut global = GlobalPriorityState::<u32>::new();
global.get_mut(1).boost_once(10.0);
global.get_mut(2).boost_once(10.0);
{
let mut m1 = global.get_mut(1);
m1.boost_once(-10.0);
}
assert_eq!(global.get_ref(1).accumulated(), 0.0);
assert_eq!(global.get_ref(2).accumulated(), 10.0,
"unsent entity 2 must retain its accumulated priority (compound-and-retain)");
}
#[test]
fn b_bdd_2_global_gain_override_wins_sort_over_default() {
let mut global = GlobalPriorityState::<u32>::new();
global.get_mut(1).set_gain(10.0);
let a_gain = global.get_ref(1).gain().unwrap_or(1.0);
let b_gain = global.get_ref(2).gain().unwrap_or(1.0);
assert!(a_gain > b_gain);
assert_eq!(a_gain, 10.0);
assert_eq!(b_gain, 1.0);
}
#[test]
fn b_bdd_3_global_user_gain_is_multiplicative() {
let mut global = GlobalPriorityState::<u32>::new();
let mut user = UserPriorityState::<u32>::new();
global.get_mut(1).set_gain(2.0);
user.get_mut(1).set_gain(5.0);
let g = global.get_ref(1).gain().unwrap_or(1.0);
let u = user.get_ref(1).gain().unwrap_or(1.0);
let effective = g * u;
assert_eq!(effective, 10.0);
}
#[test]
fn b_bdd_3_effective_gain_default_when_missing() {
let global = GlobalPriorityState::<u32>::new();
let user = UserPriorityState::<u32>::new();
let g = global.get_ref(42).gain().unwrap_or(1.0);
let u = user.get_ref(42).gain().unwrap_or(1.0);
assert_eq!(g * u, 1.0, "missing layers collapse to default 1.0 × 1.0 = 1.0");
}
#[test]
fn b_bdd_7_starvation_bound_is_structural() {
let budget_per_tick: f64 = 64_000.0 / 60.0; const MTU: f64 = 430.0;
let packets_per_tick = (budget_per_tick / MTU).floor() as u32; let n_entities: u32 = 1000;
let worst_case_wait_ticks = n_entities / packets_per_tick.max(1);
assert!(worst_case_wait_ticks < u32::MAX / 2);
assert!(worst_case_wait_ticks <= 500);
}
#[test]
fn b_bdd_9_scope_exit_evicts_only_that_users_layer() {
let mut global = GlobalPriorityState::<u32>::new();
let mut user_x = UserPriorityState::<u32>::new();
let mut user_y = UserPriorityState::<u32>::new();
global.get_mut(1).set_gain(3.0);
user_x.get_mut(1).set_gain(5.0);
user_y.get_mut(1).set_gain(7.0);
user_x.on_scope_exit(&1);
assert_eq!(user_x.get_ref(1).gain(), None, "X's per-user entry must be evicted");
assert_eq!(user_y.get_ref(1).gain(), Some(7.0), "Y's per-user entry untouched");
assert_eq!(global.get_ref(1).gain(), Some(3.0), "global layer untouched");
}
#[test]
fn b_bdd_10_despawn_evicts_global_entry() {
let mut global = GlobalPriorityState::<u32>::new();
global.get_mut(1).set_gain(5.0);
global.get_mut(1).boost_once(42.0);
assert_eq!(global.get_ref(1).gain(), Some(5.0));
assert_eq!(global.get_ref(1).accumulated(), 42.0);
global.on_despawn(&1);
assert_eq!(global.get_ref(1).gain(), None, "despawn must clear gain");
assert_eq!(global.get_ref(1).accumulated(), 0.0, "despawn must clear accumulator");
}
#[test]
fn b_bdd_5_reset_on_send_preserves_gain() {
use std::collections::HashMap;
let mut entries: HashMap<u32, EntityPriorityData> = HashMap::new();
{
let mut m = EntityPriorityMut { entries: &mut entries, entity: 1 };
m.set_gain(3.0);
m.boost_once(100.0);
assert_eq!(m.accumulated(), 100.0);
assert_eq!(m.gain(), Some(3.0));
}
entries.get_mut(&1).unwrap().accumulated = 0.0;
let m2 = EntityPriorityMut { entries: &mut entries, entity: 1 };
assert_eq!(m2.accumulated(), 0.0, "reset-on-send zeroed accumulator");
assert_eq!(m2.gain(), Some(3.0), "reset-on-send did NOT touch gain override");
}