use crate::builder::SimulationBuilder;
use crate::components::ElevatorPhase;
use crate::dispatch::scan::ScanDispatch;
use crate::entity::ElevatorId;
use crate::error::SimError;
use crate::events::Event;
use crate::stop::StopId;
use super::helpers::default_config;
fn build_sim() -> crate::sim::Simulation {
SimulationBuilder::from_config(default_config())
.dispatch(ScanDispatch::new())
.build()
.unwrap()
}
fn first_elevator(sim: &crate::sim::Simulation) -> crate::entity::ElevatorId {
crate::entity::ElevatorId::from(sim.world().elevator_ids()[0])
}
#[test]
fn fresh_queue_is_empty() {
let sim = build_sim();
let elev = first_elevator(&sim);
assert_eq!(sim.destination_queue(elev), Some(&[][..]));
}
#[test]
fn dispatch_populates_queue() {
let mut sim = build_sim();
sim.spawn_rider(StopId(1), StopId(2), 75.0).unwrap();
sim.step();
let elev = first_elevator(&sim);
let queue = sim.destination_queue(elev).unwrap();
assert!(
!queue.is_empty(),
"queue should contain the dispatched target (got {queue:?})"
);
}
#[test]
fn queue_pops_on_arrival() {
let mut sim = build_sim();
sim.spawn_rider(StopId(0), StopId(2), 75.0).unwrap();
for _ in 0..2000 {
sim.step();
let elev = first_elevator(&sim);
let car = sim.world().elevator(elev.entity()).unwrap();
if !matches!(car.phase(), ElevatorPhase::MovingToStop(_))
&& sim.destination_queue(elev).is_some_and(<[_]>::is_empty)
{
break;
}
}
let elev = first_elevator(&sim);
assert!(sim.destination_queue(elev).unwrap().is_empty());
}
#[test]
fn push_destination_adds_to_back() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let s1 = sim.stop_entity(StopId(1)).unwrap();
sim.push_destination(elev, s1).unwrap();
assert_eq!(sim.destination_queue(elev).unwrap(), &[s1]);
}
#[test]
fn push_destination_adjacent_dedup_back() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let s1 = sim.stop_entity(StopId(1)).unwrap();
sim.push_destination(elev, s1).unwrap();
sim.push_destination(elev, s1).unwrap();
assert_eq!(sim.destination_queue(elev).unwrap(), &[s1]);
}
#[test]
fn push_destination_front_inserts_at_index_0() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let s1 = sim.stop_entity(StopId(1)).unwrap();
let s2 = sim.stop_entity(StopId(2)).unwrap();
sim.push_destination(elev, s1).unwrap();
sim.push_destination_front(elev, s2).unwrap();
assert_eq!(sim.destination_queue(elev).unwrap(), &[s2, s1]);
}
#[test]
fn push_destination_front_adjacent_dedup() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let s1 = sim.stop_entity(StopId(1)).unwrap();
sim.push_destination_front(elev, s1).unwrap();
sim.push_destination_front(elev, s1).unwrap();
assert_eq!(sim.destination_queue(elev).unwrap(), &[s1]);
}
#[test]
fn clear_destinations_empties_queue() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let s0 = sim.stop_entity(StopId(0)).unwrap();
let s1 = sim.stop_entity(StopId(1)).unwrap();
let s2 = sim.stop_entity(StopId(2)).unwrap();
sim.push_destination(elev, s1).unwrap();
sim.push_destination(elev, s2).unwrap();
sim.push_destination(elev, s0).unwrap();
sim.clear_destinations(elev).unwrap();
assert!(sim.destination_queue(elev).unwrap().is_empty());
}
#[test]
fn imperative_push_drives_elevator() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let s2 = sim.stop_entity(StopId(2)).unwrap();
sim.push_destination(elev, s2).unwrap();
let mut arrived = false;
for _ in 0..2000 {
sim.step();
for ev in sim.drain_events() {
if let Event::ElevatorArrived {
elevator, at_stop, ..
} = ev
&& elevator == elev.entity()
&& at_stop == s2
{
arrived = true;
}
}
if arrived {
break;
}
}
assert!(
arrived,
"elevator should have arrived at stop 2 via imperative queue"
);
}
#[test]
fn push_front_overrides_current_target() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let s1 = sim.stop_entity(StopId(1)).unwrap();
let s2 = sim.stop_entity(StopId(2)).unwrap();
sim.push_destination(elev, s2).unwrap();
sim.step(); sim.step();
sim.push_destination_front(elev, s1).unwrap();
let mut arrived_at = None;
for _ in 0..2000 {
sim.step();
for ev in sim.drain_events() {
if let Event::ElevatorArrived {
elevator, at_stop, ..
} = ev
&& elevator == elev.entity()
{
arrived_at = Some(at_stop);
break;
}
}
if arrived_at.is_some() {
break;
}
}
assert_eq!(
arrived_at,
Some(s1),
"elevator should arrive at s1 first after push_front"
);
}
#[test]
fn destination_queued_event_fires_on_push() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let s2 = sim.stop_entity(StopId(2)).unwrap();
sim.push_destination(elev, s2).unwrap();
sim.spawn_rider(StopId(1), StopId(2), 75.0).unwrap();
sim.step();
let events = sim.drain_events();
let count = events
.iter()
.filter(|e| matches!(e, Event::DestinationQueued { .. }))
.count();
assert!(
count >= 2,
"expected at least 2 DestinationQueued events, got {count}"
);
}
#[test]
fn destination_queued_event_suppressed_on_dedup() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let s1 = sim.stop_entity(StopId(1)).unwrap();
sim.push_destination(elev, s1).unwrap();
sim.push_destination(elev, s1).unwrap();
let events = sim.drain_events();
let count = events
.iter()
.filter(|e| matches!(e, Event::DestinationQueued { .. }))
.count();
assert_eq!(count, 1);
}
#[test]
fn snapshot_roundtrip_preserves_queue() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let s0 = sim.stop_entity(StopId(0)).unwrap();
let s1 = sim.stop_entity(StopId(1)).unwrap();
let s2 = sim.stop_entity(StopId(2)).unwrap();
sim.push_destination(elev, s1).unwrap();
sim.push_destination(elev, s2).unwrap();
sim.push_destination(elev, s0).unwrap();
let snapshot = sim.snapshot();
let restored = snapshot.restore(None).unwrap();
let new_elev = ElevatorId::from(restored.world().elevator_ids()[0]);
let restored_queue = restored.destination_queue(new_elev).unwrap();
assert_eq!(restored_queue.len(), 3);
}
#[test]
fn push_destination_errors_on_non_elevator() {
let mut sim = build_sim();
let s1 = sim.stop_entity(StopId(1)).unwrap();
let rider = sim.spawn_rider(StopId(0), StopId(2), 75.0).unwrap();
let result = sim.push_destination(ElevatorId::from(rider.entity()), s1);
assert!(matches!(result, Err(SimError::NotAnElevator(_))));
}
#[test]
fn push_destination_errors_on_non_stop() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
let result = sim.push_destination(elev, elev.entity());
assert!(matches!(result, Err(SimError::NotAStop(_))));
}
#[test]
fn redirect_via_push_front_updates_direction_indicators() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
sim.spawn_rider(StopId(1), StopId(2), 75.0).unwrap();
for _ in 0..20 {
sim.step();
if matches!(
sim.world()
.elevator(elev.entity())
.map(crate::components::Elevator::phase),
Some(ElevatorPhase::MovingToStop(_))
) {
break;
}
}
assert_eq!(sim.elevator_going_up(elev.entity()), Some(true));
assert_eq!(sim.elevator_going_down(elev.entity()), Some(false));
let stop_0 = sim.stop_entity(StopId(0)).unwrap();
sim.push_destination_front(elev, stop_0).unwrap();
sim.step();
assert_eq!(
sim.elevator_going_up(elev.entity()),
Some(false),
"push_destination_front to a lower stop must clear going_up",
);
assert_eq!(sim.elevator_going_down(elev.entity()), Some(true));
}
#[test]
fn recall_to_clears_queue_and_sets_target() {
let mut sim = SimulationBuilder::demo().build().unwrap();
let elev = ElevatorId::from(sim.world().elevator_ids()[0]);
sim.push_destination(elev, StopId(1)).unwrap();
sim.recall_to(elev, StopId(0)).unwrap();
let q = sim.destination_queue(elev).unwrap();
assert_eq!(q.len(), 1, "queue should contain only the recall target");
assert_eq!(q[0], sim.stop_entity(StopId(0)).unwrap());
}
#[test]
fn recall_to_emits_event() {
let mut sim = SimulationBuilder::demo().build().unwrap();
let elev = ElevatorId::from(sim.world().elevator_ids()[0]);
sim.drain_events();
sim.recall_to(elev, StopId(1)).unwrap();
let recall_events: Vec<_> = sim
.drain_events()
.into_iter()
.filter(|e| matches!(e, Event::ElevatorRecalled { .. }))
.collect();
assert_eq!(recall_events.len(), 1);
if let Event::ElevatorRecalled {
elevator, to_stop, ..
} = &recall_events[0]
{
assert_eq!(*elevator, elev.entity());
assert_eq!(*to_stop, sim.stop_entity(StopId(1)).unwrap());
}
}
#[test]
fn recall_idle_car_to_distant_stop() {
let mut sim = SimulationBuilder::demo().build().unwrap();
let elev = ElevatorId::from(sim.world().elevator_ids()[0]);
sim.recall_to(elev, StopId(1)).unwrap();
let target_pos = sim
.world()
.stop(sim.stop_entity(StopId(1)).unwrap())
.unwrap()
.position();
let mut arrived = false;
for _ in 0..2000 {
sim.step();
let pos = sim.world().position(elev.entity()).unwrap().value;
if (pos - target_pos).abs() < 0.01 {
arrived = true;
break;
}
}
assert!(arrived, "car should have arrived at the recall stop");
}
#[test]
fn recall_to_current_stop_opens_doors() {
let mut sim = SimulationBuilder::demo().build().unwrap();
let elev = ElevatorId::from(sim.world().elevator_ids()[0]);
sim.recall_to(elev, StopId(0)).unwrap();
let mut saw_open = false;
for _ in 0..30 {
sim.step();
let car = sim.world().elevator(elev.entity()).unwrap();
if car.door().is_open() {
saw_open = true;
break;
}
}
assert!(saw_open, "doors should open when recalled to current stop");
}
#[test]
fn recall_works_on_independent_car() {
let mut sim = SimulationBuilder::demo().build().unwrap();
let elev = ElevatorId::from(sim.world().elevator_ids()[0]);
sim.set_service_mode(elev.entity(), crate::components::ServiceMode::Independent)
.unwrap();
sim.recall_to(elev, StopId(1)).unwrap();
let target_pos = sim
.world()
.stop(sim.stop_entity(StopId(1)).unwrap())
.unwrap()
.position();
let mut arrived = false;
for _ in 0..2000 {
sim.step();
let pos = sim.world().position(elev.entity()).unwrap().value;
if (pos - target_pos).abs() < 0.01 {
arrived = true;
break;
}
}
assert!(arrived, "Independent car should still respond to recall_to");
}
#[test]
fn recall_to_validates_entities() {
let mut sim = SimulationBuilder::demo().build().unwrap();
let elev = ElevatorId::from(sim.world().elevator_ids()[0]);
let stop_entity = sim.stop_entity(StopId(0)).unwrap();
assert!(matches!(
sim.recall_to(ElevatorId::from(stop_entity), StopId(0)),
Err(SimError::NotAnElevator(_))
));
assert!(sim.recall_to(elev, StopId(99)).is_err());
}
#[test]
fn recall_mid_flight_redirects() {
let mut sim = SimulationBuilder::demo().build().unwrap();
let elev = ElevatorId::from(sim.world().elevator_ids()[0]);
sim.push_destination(elev, StopId(1)).unwrap();
for _ in 0..10 {
sim.step();
if sim
.world()
.elevator(elev.entity())
.unwrap()
.phase()
.is_moving()
{
break;
}
}
assert!(
sim.world()
.elevator(elev.entity())
.unwrap()
.phase()
.is_moving(),
"car should be in flight"
);
sim.recall_to(elev, StopId(0)).unwrap();
let stop0_pos = sim
.world()
.stop(sim.stop_entity(StopId(0)).unwrap())
.unwrap()
.position();
let mut returned = false;
for _ in 0..2000 {
sim.step();
let pos = sim.world().position(elev.entity()).unwrap().value;
let phase = sim.world().elevator(elev.entity()).unwrap().phase();
if (pos - stop0_pos).abs() < 0.01 && !phase.is_moving() {
returned = true;
break;
}
}
assert!(
returned,
"car should return to stop 0 after mid-flight recall"
);
}
#[test]
fn disable_stop_scrubs_destination_queues() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
sim.push_destination(elev, StopId(1)).unwrap();
sim.push_destination(elev, StopId(2)).unwrap();
let stop1_entity = sim.stop_entity(StopId(1)).unwrap();
sim.disable(stop1_entity).unwrap();
let q = sim.destination_queue(elev).unwrap();
assert!(
!q.contains(&stop1_entity),
"disabled stop should be scrubbed from destination queue"
);
assert_eq!(q.len(), 1, "only the non-disabled stop should remain");
}
#[test]
fn disable_stop_resets_inflight_car() {
let mut sim = build_sim();
let elev = first_elevator(&sim);
sim.push_destination(elev, StopId(2)).unwrap();
for _ in 0..10 {
sim.step();
if sim
.world()
.elevator(elev.entity())
.unwrap()
.phase()
.is_moving()
{
break;
}
}
assert!(
sim.world()
.elevator(elev.entity())
.unwrap()
.phase()
.is_moving(),
"car should be in flight"
);
let stop2_entity = sim.stop_entity(StopId(2)).unwrap();
sim.disable(stop2_entity).unwrap();
let car = sim.world().elevator(elev.entity()).unwrap();
assert_eq!(
car.phase(),
ElevatorPhase::Idle,
"car targeting a disabled stop should be reset to Idle"
);
assert_eq!(
car.target_stop(),
None,
"target_stop should be cleared for the disabled stop"
);
}