use crate::arrival_log::{ArrivalLog, DestinationLog};
use crate::entity::EntityId;
use crate::traffic_detector::{TrafficDetector, TrafficMode};
use crate::world::World;
use super::helpers::{default_config, run_until_done, scan};
use crate::sim::Simulation;
use crate::stop::StopId;
fn fake_stops() -> (World, Vec<EntityId>) {
let mut world = World::new();
let lobby = world.spawn();
let f2 = world.spawn();
let f3 = world.spawn();
(world, vec![lobby, f2, f3])
}
#[test]
fn default_mode_is_idle() {
let d = TrafficDetector::new();
assert_eq!(d.current_mode(), TrafficMode::Idle);
}
#[test]
fn empty_log_stays_idle() {
let mut d = TrafficDetector::new();
let log = ArrivalLog::default();
let (_w, stops) = fake_stops();
d.update(&log, &DestinationLog::default(), 60 * 60 * 10, &stops);
assert_eq!(d.current_mode(), TrafficMode::Idle);
}
#[test]
fn up_peak_trips_on_lobby_fraction() {
let mut d = TrafficDetector::new().with_window_ticks(3_600);
let mut log = ArrivalLog::default();
let (_w, stops) = fake_stops();
let lobby = stops[0];
let f2 = stops[1];
let f3 = stops[2];
for t in 0..70u64 {
log.record(t * 50, lobby);
}
for t in 0..15u64 {
log.record(t * 50, f2);
log.record(t * 50, f3);
}
d.update(&log, &DestinationLog::default(), 3_500, &[lobby, f2, f3]);
assert_eq!(d.current_mode(), TrafficMode::UpPeak);
}
#[test]
fn inter_floor_uniform_distribution() {
let mut d = TrafficDetector::new().with_window_ticks(3_600);
let mut log = ArrivalLog::default();
let (_w, stops) = fake_stops();
for t in 0..60u64 {
for &s in &stops {
log.record(t * 10, s);
}
}
d.update(&log, &DestinationLog::default(), 3_500, &stops);
assert_eq!(d.current_mode(), TrafficMode::InterFloor);
}
#[test]
fn idle_rate_overrides_lobby_fraction() {
let mut d = TrafficDetector::new().with_window_ticks(3_600);
let mut log = ArrivalLog::default();
let (_w, stops) = fake_stops();
let lobby = stops[0];
let f2 = stops[1];
log.record(100, lobby);
d.update(&log, &DestinationLog::default(), 3_500, &[lobby, f2]);
assert_eq!(d.current_mode(), TrafficMode::Idle);
}
#[test]
fn no_stops_is_idle() {
let mut d = TrafficDetector::new();
d.update(
&ArrivalLog::default(),
&DestinationLog::default(),
1_000,
&[],
);
assert_eq!(d.current_mode(), TrafficMode::Idle);
}
#[test]
fn zero_threshold_with_empty_window_stays_idle() {
let mut d = TrafficDetector::new().with_idle_rate_threshold(0.0);
let (_w, stops) = fake_stops();
d.update(
&ArrivalLog::default(),
&DestinationLog::default(),
3_600,
&stops,
);
assert_eq!(d.current_mode(), TrafficMode::Idle);
}
#[test]
fn down_peak_trips_on_lobby_destination_fraction() {
let mut d = TrafficDetector::new().with_window_ticks(3_600);
let mut arrivals = ArrivalLog::default();
let mut destinations = DestinationLog::default();
let (_w, stops) = fake_stops();
let lobby = stops[0];
let f2 = stops[1];
let f3 = stops[2];
for t in 0..30u64 {
arrivals.record(t * 50, f2);
arrivals.record(t * 50, f3);
}
for t in 0..45u64 {
destinations.record(t * 50, lobby);
}
for t in 0..15u64 {
destinations.record(t * 50, f2);
}
d.update(&arrivals, &destinations, 3_500, &[lobby, f2, f3]);
assert_eq!(d.current_mode(), TrafficMode::DownPeak);
}
#[test]
fn up_peak_beats_down_peak_when_both_trigger() {
let mut d = TrafficDetector::new().with_window_ticks(3_600);
let mut arrivals = ArrivalLog::default();
let mut destinations = DestinationLog::default();
let (_w, stops) = fake_stops();
let lobby = stops[0];
let f2 = stops[1];
for t in 0..100u64 {
arrivals.record(t * 30, lobby);
destinations.record(t * 30, lobby);
}
d.update(&arrivals, &destinations, 3_500, &[lobby, f2]);
assert_eq!(d.current_mode(), TrafficMode::UpPeak);
}
#[test]
#[should_panic(expected = "down_peak_fraction must be finite and in [0, 1]")]
fn down_peak_fraction_out_of_range_panics() {
let _ = TrafficDetector::new().with_down_peak_fraction(-0.1);
}
#[test]
#[should_panic(expected = "TrafficDetector::with_window_ticks requires a positive window")]
fn zero_window_panics() {
let _ = TrafficDetector::new().with_window_ticks(0);
}
#[test]
#[should_panic(expected = "up_peak_fraction must be finite and in [0, 1]")]
fn out_of_range_up_peak_fraction_panics() {
let _ = TrafficDetector::new().with_up_peak_fraction(1.5);
}
#[test]
#[should_panic(expected = "idle_rate_threshold must be finite and non-negative")]
fn nan_idle_rate_panics() {
let _ = TrafficDetector::new().with_idle_rate_threshold(f64::NAN);
}
#[test]
fn simulation_installs_traffic_detector_resource() {
let sim = Simulation::new(&default_config(), scan()).unwrap();
let present = sim.world().resource::<TrafficDetector>().is_some();
assert!(
present,
"Simulation::new must insert a TrafficDetector resource by default"
);
}
#[test]
fn simulation_installs_both_arrival_and_destination_logs() {
let sim = Simulation::new(&default_config(), scan()).unwrap();
assert!(
sim.world().resource::<ArrivalLog>().is_some(),
"Simulation::new must insert an ArrivalLog resource"
);
assert!(
sim.world().resource::<DestinationLog>().is_some(),
"Simulation::new must insert a DestinationLog resource — \
missing it silently disables DownPeak detection"
);
}
#[test]
fn rider_spawn_appends_to_both_logs() {
let mut sim = Simulation::new(&default_config(), scan()).unwrap();
sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
let arrivals_len = sim.world().resource::<ArrivalLog>().unwrap().len();
let destinations_len = sim.world().resource::<DestinationLog>().unwrap().len();
assert_eq!(arrivals_len, 1, "arrival log must carry 1 entry");
assert_eq!(destinations_len, 1, "destination log must carry 1 entry");
}
#[test]
fn metrics_phase_refreshes_detector_last_update_tick() {
let mut sim = Simulation::new(&default_config(), scan()).unwrap();
sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
let _ = run_until_done(&mut sim, 20_000);
let now = sim.current_tick();
let detector = sim
.world()
.resource::<TrafficDetector>()
.unwrap_or_else(|| panic!("detector installed"));
let delta = now.saturating_sub(detector.last_update_tick());
assert!(
delta <= 1,
"metrics phase must refresh the detector every tick (delta={delta}, now={now})"
);
assert!(
detector.last_update_tick() > 0,
"detector was never updated"
);
}