//! Tests for the new public API surface: `remove_elevator`, `remove_stop`,
//! `drain_events_where`, `riders_on`, `occupancy`, `iter_repositioning_elevators`,
//! `RiderBuilder`, and dispatch re-exports.

use super::helpers::{default_config, scan};
use crate::builder::SimulationBuilder;
use crate::components::{Accel, AccessControl, Preferences, RiderPhase, Speed, Weight};
use crate::dispatch::BuiltinReposition;
use crate::dispatch::reposition::ReturnToLobby;
use crate::dispatch::{EtdDispatch, LookDispatch, NearestCarDispatch, ScanDispatch};
use crate::entity::{ElevatorId, EntityId};
use crate::error::SimError;
use crate::events::Event;
use crate::ids::GroupId;
use crate::sim::Simulation;
use crate::stop::{StopConfig, StopId};
use std::collections::HashSet;

// ── remove_elevator ───────────────────────────────────────────────────────────

#[test]
fn remove_elevator_despawns_from_world() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let elevator_id = sim.groups()[0].elevator_entities()[0];

    // Elevator is alive before removal.
    assert!(sim.world().elevator(elevator_id).is_some());

    sim.remove_elevator(elevator_id).unwrap();

    // Elevator is gone from the world.
    assert!(sim.world().elevator(elevator_id).is_none());
}

#[test]
fn remove_elevator_removes_from_group_cache() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let elevator_id = sim.groups()[0].elevator_entities()[0];
    assert!(sim.groups()[0].elevator_entities().contains(&elevator_id));

    sim.remove_elevator(elevator_id).unwrap();

    assert!(!sim.groups()[0].elevator_entities().contains(&elevator_id));
}

#[test]
fn remove_elevator_ejects_riders_aboard() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    // Spawn a rider and let it board.
    let rider_id = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();

    // Run enough ticks for the rider to board.
    for _ in 0..300 {
        sim.step();
        let phase = sim.world().rider(rider_id.entity()).unwrap().phase;
        if matches!(phase, RiderPhase::Riding(_)) {
            break;
        }
    }

    let elevator_id = sim.groups()[0].elevator_entities()[0];
    assert!(
        matches!(
            sim.world().rider(rider_id.entity()).unwrap().phase,
            RiderPhase::Riding(_)
        ),
        "rider should have boarded within 300 ticks"
    );

    // Rider is aboard — removing elevator should eject the rider.
    sim.remove_elevator(elevator_id).unwrap();
    sim.drain_events(); // consume events

    let phase = sim.world().rider(rider_id.entity()).unwrap().phase;
    // After ejection, rider is put back to Waiting.
    assert!(
        matches!(phase, RiderPhase::Waiting),
        "rider should be Waiting after elevator removal, got {phase:?}"
    );
}

/// `remove_elevator` must keep the rider population index coherent: any rider
/// transitioned back to `Waiting` as part of the ejection must be discoverable
/// via `waiting_at(stop)` for the stop they were placed at. Without this
/// guarantee, dispatch and `RiderIndex` queries would silently miss them and
/// the rider becomes a "ghost" — phase says Waiting but they're absent from
/// every per-stop population query.
#[test]
fn remove_elevator_keeps_rider_index_coherent() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();

    // Advance until the rider is mid-ride.
    for _ in 0..300 {
        sim.step();
        if matches!(
            sim.world().rider(rider_id.entity()).unwrap().phase,
            RiderPhase::Riding(_)
        ) {
            break;
        }
    }
    assert!(
        matches!(
            sim.world().rider(rider_id.entity()).unwrap().phase,
            RiderPhase::Riding(_)
        ),
        "rider should have boarded within 300 ticks"
    );

    let elevator_id = sim.groups()[0].elevator_entities()[0];
    sim.remove_elevator(elevator_id).unwrap();
    sim.drain_events();

    // Rider must now be Waiting at some stop with a coherent current_stop.
    let rider = sim.world().rider(rider_id.entity()).unwrap();
    assert!(
        matches!(rider.phase, RiderPhase::Waiting),
        "rider phase should be Waiting, got {:?}",
        rider.phase
    );
    let stop = rider
        .current_stop
        .expect("ejected rider must land at a stop, not nowhere");

    // The invariant: phase==Waiting + current_stop==Some(s) implies the
    // rider is in `waiting_at(s)`. The population index is the canonical
    // source of truth for dispatch.
    let waiting: Vec<EntityId> = sim.waiting_at(stop).collect();
    assert!(
        waiting.contains(&rider_id.entity()),
        "rider {:?} is Waiting at stop {:?} but missing from waiting_at; \
         rider_index drifted from rider.phase \
         (waiting_at returned {:?})",
        rider_id.entity(),
        stop,
        waiting
    );
}

/// Pins down the post-refactor contract for `World::despawn` on a populated
/// elevator: it does **not** silently rewrite rider phase. The previous
/// behaviour rewrote phase to Waiting (without updating `RiderIndex`),
/// producing ghost riders. The new low-level contract is documented in
/// `World::despawn`; the proper way to remove a populated elevator is
/// `Simulation::remove_elevator` (covered by
/// [`remove_elevator_keeps_rider_index_coherent`] above).
#[test]
fn world_despawn_elevator_does_not_silently_rewrite_rider_phase() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    for _ in 0..300 {
        sim.step();
        if matches!(
            sim.world().rider(rider_id.entity()).unwrap().phase,
            RiderPhase::Riding(_)
        ) {
            break;
        }
    }
    assert!(
        matches!(
            sim.world().rider(rider_id.entity()).unwrap().phase,
            RiderPhase::Riding(_)
        ),
        "rider should have boarded within 300 ticks"
    );

    let elevator_id = sim.groups()[0].elevator_entities()[0];

    // Bypass `Simulation::remove_elevator`. Pre-refactor this rewrote
    // rider.phase to Waiting (without updating RiderIndex) — a "ghost"
    // rider. Post-refactor `World::despawn` no longer touches rider phase,
    // making the inconsistency visible to the caller rather than silently
    // half-applied.
    sim.world_mut().despawn(elevator_id);

    assert!(!sim.world().is_alive(elevator_id));

    let rider = sim.world().rider(rider_id.entity()).unwrap();
    assert!(
        matches!(rider.phase, RiderPhase::Riding(_)),
        "rider phase should be unchanged by raw World::despawn, got {:?}",
        rider.phase
    );
    // Importantly: there is no ghost in any rider_index bucket — the
    // rider was never indexed (Riding isn't an indexed phase) and
    // World::despawn no longer attempts a phase rewrite that would have
    // introduced one.
    let stops: Vec<EntityId> = sim.world().iter_stops().map(|(eid, _)| eid).collect();
    for stop in stops {
        assert!(
            !sim.waiting_at(stop).any(|r| r == rider_id.entity()),
            "rider {:?} should not appear in waiting_at after raw despawn",
            rider_id.entity()
        );
    }
}

#[test]
fn remove_elevator_ejects_rider_emits_event() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();
    sim.drain_events();

    let rider_id = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();

    // Run until rider is riding.
    for _ in 0..300 {
        sim.step();
        if matches!(
            sim.world().rider(rider_id.entity()).unwrap().phase,
            RiderPhase::Riding(_)
        ) {
            break;
        }
    }

    let elevator_id = sim.groups()[0].elevator_entities()[0];

    assert!(
        matches!(
            sim.world().rider(rider_id.entity()).unwrap().phase,
            RiderPhase::Riding(_)
        ),
        "rider should have boarded within 300 ticks"
    );

    sim.remove_elevator(elevator_id).unwrap();
    let events = sim.drain_events();

    let ejected = events
        .iter()
        .any(|e| matches!(e, Event::RiderEjected { rider, .. } if *rider == rider_id.entity()));
    assert!(
        ejected,
        "should emit RiderEjected when removing elevator with rider aboard"
    );
}

#[test]
fn remove_nonexistent_elevator_returns_entity_not_found() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let fake_id = EntityId::default();
    let result = sim.remove_elevator(fake_id);
    assert!(
        matches!(result, Err(SimError::EntityNotFound(_))),
        "expected EntityNotFound, got {result:?}"
    );
}

// ── remove_stop ───────────────────────────────────────────────────────────────

#[test]
fn remove_stop_despawns_from_world() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let stop_id = sim.stop_entity(StopId(2)).unwrap();
    assert!(sim.world().stop(stop_id).is_some());

    sim.remove_stop(stop_id).unwrap();

    assert!(sim.world().stop(stop_id).is_none());
}

#[test]
fn remove_stop_removes_from_group_stop_cache() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let stop_id = sim.stop_entity(StopId(2)).unwrap();
    assert!(sim.groups()[0].stop_entities().contains(&stop_id));

    sim.remove_stop(stop_id).unwrap();

    assert!(!sim.groups()[0].stop_entities().contains(&stop_id));
}

#[test]
fn remove_stop_removes_from_stop_lookup() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let stop_id = sim.stop_entity(StopId(2)).unwrap();
    assert!(stop_id != EntityId::default());

    sim.remove_stop(stop_id).unwrap();

    // After removal, stop_entity should return None.
    let after = sim.stop_entity(StopId(2));
    assert!(
        after.is_none(),
        "stop_entity should return None after removal"
    );
}

#[test]
fn remove_stop_with_waiting_rider_invalidates_route() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();
    sim.drain_events();

    // Spawn a rider targeting stop 2.
    let rider_id = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();

    // Ensure the rider is in Waiting phase (before boarding).
    let phase = sim.world().rider(rider_id.entity()).unwrap().phase;
    assert_eq!(phase, RiderPhase::Waiting);

    // Remove the destination stop.
    let stop2 = sim.stop_entity(StopId(2)).unwrap();
    sim.remove_stop(stop2).unwrap();

    let events = sim.drain_events();
    // Removing a stop should emit RouteInvalidated for any rider whose route references it.
    let invalidated = events
        .iter()
        .any(|e| matches!(e, Event::RouteInvalidated { rider, .. } if *rider == rider_id.entity()));
    assert!(
        invalidated,
        "should emit RouteInvalidated for rider targeting the removed stop"
    );
}

/// `remove_stop` must not leave dangling references in elevator state.
/// Pre-fix: `target_stop`, `DestinationQueue`, and `restricted_stops` all
/// kept pointing at the despawned `EntityId`, which caused subsequent
/// `movement`/`advance_queue` phases to ask `world.stop_position(target_stop)`
/// and get `None`, potentially stalling the elevator.
#[test]
fn remove_stop_clears_dangling_references_on_elevator() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let stop2 = sim.stop_entity(StopId(2)).unwrap();
    let elev = sim.groups()[0].elevator_entities()[0];

    // Queue stop 2 as a destination, then dispatch so the elevator picks
    // it as its target.
    sim.push_destination(ElevatorId::from(elev), stop2).unwrap();
    sim.step();

    // Seed the restricted_stops set directly (normally populated via
    // config but we want to cover the cleanup path).
    if let Some(car) = sim.world_mut().elevator_mut(elev) {
        car.restricted_stops.insert(stop2);
    }

    // Sanity: the references exist before removal.
    let car = sim.world().elevator(elev).unwrap();
    assert!(
        car.target_stop == Some(stop2)
            || sim
                .destination_queue(ElevatorId::from(elev))
                .is_some_and(|q| q.contains(&stop2)),
        "test precondition: elevator should reference stop2 somehow"
    );
    assert!(car.restricted_stops.contains(&stop2));

    sim.remove_stop(stop2).unwrap();

    let car = sim.world().elevator(elev).unwrap();
    assert_ne!(
        car.target_stop,
        Some(stop2),
        "target_stop must be cleared when the referenced stop is removed"
    );
    if let Some(q) = sim.destination_queue(ElevatorId::from(elev)) {
        assert!(
            !q.contains(&stop2),
            "DestinationQueue must not contain the removed stop"
        );
    }
    assert!(
        !car.restricted_stops.contains(&stop2),
        "restricted_stops must not contain the removed stop"
    );
}

/// Pre-fix, `remove_stop` left `CarCall.floor` references pointing at the
/// despawned stop entity — visible through the public `sim.car_calls()`
/// accessor and consumed by custom dispatch strategies. (#293)
#[test]
fn remove_stop_drops_car_calls_referencing_stop() {
    use crate::components::CarCall;

    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let stop2 = sim.stop_entity(StopId(2)).unwrap();
    let elev = sim.groups()[0].elevator_entities()[0];

    // Pre-load a CarCall whose floor is stop2.
    let tick = sim.current_tick();
    sim.world_mut()
        .car_calls_mut(elev)
        .unwrap()
        .push(CarCall::new(elev, stop2, tick));
    assert!(
        sim.car_calls(ElevatorId::from(elev))
            .iter()
            .any(|c| c.floor == stop2),
        "precondition: car has a CarCall pointing at stop2"
    );

    sim.remove_stop(stop2).unwrap();

    assert!(
        sim.car_calls(ElevatorId::from(elev))
            .iter()
            .all(|c| c.floor != stop2),
        "remove_stop must drop CarCall entries whose floor is the removed stop"
    );
}

#[test]
fn remove_nonexistent_stop_returns_entity_not_found() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let fake_id = EntityId::default();
    let result = sim.remove_stop(fake_id);
    assert!(
        matches!(result, Err(SimError::EntityNotFound(_))),
        "expected EntityNotFound, got {result:?}"
    );
}

// ── drain_events_where ────────────────────────────────────────────────────────

/// `pending_events` and `drain_events` must agree on what events exist.
/// Pre-fix, `pending_events` returned only the post-step output buffer
/// and missed events emitted outside the tick loop (`spawn_rider`,
/// `disable`, etc.) — `drain_events` flushed first so it saw them.
/// (#264)
#[test]
fn pending_events_matches_drain_events() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    // Emit RiderSpawned outside the tick loop. Pre-fix, peek would be
    // empty while drain returned [RiderSpawned] — peek and drain disagreed.
    sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();

    let peek_count = sim.pending_events().len();
    assert!(
        peek_count >= 1,
        "pending_events should flush the event bus and see RiderSpawned, got {peek_count}"
    );

    let drained = sim.drain_events();
    assert_eq!(
        drained.len(),
        peek_count,
        "drain must yield the same count peek reported"
    );
    assert!(
        drained
            .iter()
            .any(|e| matches!(e, Event::RiderSpawned { .. }))
    );
}

#[test]
fn drain_events_where_returns_only_matching_events() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    // Spawn a rider to generate events.
    sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    // Run a few ticks to generate events.
    for _ in 0..5 {
        sim.step();
    }

    // Drain only ElevatorAssigned events.
    let matched = sim.drain_events_where(|e| matches!(e, Event::ElevatorAssigned { .. }));

    // All returned events must be ElevatorAssigned.
    for e in &matched {
        assert!(
            matches!(e, Event::ElevatorAssigned { .. }),
            "unexpected event in matched set: {e:?}"
        );
    }
}

#[test]
fn drain_events_where_retains_non_matching_events() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    for _ in 0..5 {
        sim.step();
    }

    // Count total events before the filter drain.
    let total_before: Vec<Event> = {
        // Peek via a drain of everything and then restore via drain_events_where(|_| true).
        // Instead, drain all first and count.
        sim.drain_events()
    };
    let total_count = total_before.len();

    // Re-run for fresh events.
    let config2 = default_config();
    let mut sim2 = Simulation::new(&config2, scan()).unwrap();
    sim2.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    for _ in 0..5 {
        sim2.step();
    }

    let matched = sim2.drain_events_where(|e| matches!(e, Event::ElevatorAssigned { .. }));
    let remaining = sim2.drain_events();

    // matched + remaining == total events.
    assert_eq!(
        matched.len() + remaining.len(),
        total_count,
        "drain_events_where + drain_events should account for all events"
    );

    // None of the remaining events should be ElevatorAssigned.
    for e in &remaining {
        assert!(
            !matches!(e, Event::ElevatorAssigned { .. }),
            "ElevatorAssigned should have been drained already: {e:?}"
        );
    }
}

#[test]
fn drain_events_where_with_no_match_returns_empty_and_retains_all() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    for _ in 0..5 {
        sim.step();
    }

    let all_before = sim.drain_events();
    let count_before = all_before.len();

    // Re-run for fresh events.
    let config2 = default_config();
    let mut sim2 = Simulation::new(&config2, scan()).unwrap();
    sim2.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    for _ in 0..5 {
        sim2.step();
    }

    // Drain with a predicate that never matches.
    let matched = sim2.drain_events_where(|_| false);
    assert!(
        matched.is_empty(),
        "should return empty vec when nothing matches"
    );

    let remaining = sim2.drain_events();
    assert_eq!(
        remaining.len(),
        count_before,
        "all events should remain after a no-match drain"
    );
}

// ── riders_on ─────────────────────────────────────────────────────────────────

#[test]
fn riders_on_returns_empty_for_idle_elevator() {
    let config = default_config();
    let sim = Simulation::new(&config, scan()).unwrap();
    let elevator_id = sim.groups()[0].elevator_entities()[0];
    assert!(sim.riders_on(elevator_id.into()).is_empty());
}

#[test]
fn riders_on_returns_rider_ids_after_boarding() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    let elevator_id = sim.groups()[0].elevator_entities()[0];

    // Run until rider is riding.
    for _ in 0..300 {
        sim.step();
        if matches!(
            sim.world().rider(rider_id.entity()).unwrap().phase,
            RiderPhase::Riding(_)
        ) {
            break;
        }
    }

    if matches!(
        sim.world().rider(rider_id.entity()).unwrap().phase,
        RiderPhase::Riding(_)
    ) {
        assert!(
            sim.riders_on(elevator_id.into())
                .contains(&rider_id.entity()),
            "rider should appear in riders_on after boarding"
        );
    }
}

#[test]
fn riders_on_returns_empty_for_nonexistent_elevator() {
    let config = default_config();
    let sim = Simulation::new(&config, scan()).unwrap();
    let fake_id = EntityId::default();
    assert!(sim.riders_on(fake_id.into()).is_empty());
}

// ── occupancy ─────────────────────────────────────────────────────────────────

#[test]
fn occupancy_returns_zero_for_idle_elevator() {
    let config = default_config();
    let sim = Simulation::new(&config, scan()).unwrap();
    let elevator_id = sim.groups()[0].elevator_entities()[0];
    assert_eq!(sim.occupancy(elevator_id.into()), 0);
}

#[test]
fn occupancy_returns_correct_count_after_boarding() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    let elevator_id = sim.groups()[0].elevator_entities()[0];

    // Run until rider is riding.
    for _ in 0..300 {
        sim.step();
        if matches!(
            sim.world().rider(rider_id.entity()).unwrap().phase,
            RiderPhase::Riding(_)
        ) {
            break;
        }
    }

    if matches!(
        sim.world().rider(rider_id.entity()).unwrap().phase,
        RiderPhase::Riding(_)
    ) {
        assert_eq!(
            sim.occupancy(elevator_id.into()),
            1,
            "occupancy should be 1 after one rider boards"
        );
    }
}

#[test]
fn occupancy_returns_zero_for_nonexistent_elevator() {
    let config = default_config();
    let sim = Simulation::new(&config, scan()).unwrap();
    let fake_id = EntityId::default();
    assert_eq!(sim.occupancy(fake_id.into()), 0);
}

// ── iter_repositioning_elevators ──────────────────────────────────────────────

#[test]
fn iter_repositioning_elevators_empty_when_no_reposition() {
    let config = default_config();
    let sim = Simulation::new(&config, scan()).unwrap();
    assert!(
        sim.iter_repositioning_elevators().next().is_none(),
        "no elevators should be repositioning without a reposition strategy"
    );
}

#[test]
fn iter_repositioning_elevators_returns_elevator_during_reposition() {
    // Build a sim with ReturnToLobby reposition strategy.
    // Elevator starts at stop 2 (top) and should reposition to stop 0 (lobby).
    let mut sim = SimulationBuilder::new()
        .stops(vec![
            StopConfig {
                id: StopId(0),
                name: "Lobby".into(),
                position: 0.0,
            },
            StopConfig {
                id: StopId(1),
                name: "Mid".into(),
                position: 10.0,
            },
            StopConfig {
                id: StopId(2),
                name: "Top".into(),
                position: 20.0,
            },
        ])
        .elevator(crate::config::ElevatorConfig {
            id: 0,
            name: "A".into(),
            max_speed: Speed::from(2.0),
            acceleration: Accel::from(1.5),
            deceleration: Accel::from(2.0),
            weight_capacity: Weight::from(800.0),
            starting_stop: StopId(2),
            door_open_ticks: 10,
            door_transition_ticks: 5,
            restricted_stops: Vec::new(),
            #[cfg(feature = "energy")]
            energy_profile: None,
            service_mode: None,
            inspection_speed_factor: 0.25,

            bypass_load_up_pct: None,

            bypass_load_down_pct: None,
        })
        .dispatch(EtdDispatch::new())
        .reposition(ReturnToLobby::new(), BuiltinReposition::ReturnToLobby)
        .build()
        .unwrap();

    sim.drain_events();
    // First step triggers reposition.
    sim.step();

    let repositioning: Vec<EntityId> = sim.iter_repositioning_elevators().collect();
    assert!(
        !repositioning.is_empty(),
        "elevator should be repositioning after first tick"
    );

    // Every ID returned must be a known elevator.
    let elevator_ids = sim.world().elevator_ids();
    for repo_id in &repositioning {
        assert!(
            elevator_ids.contains(repo_id),
            "iter_repositioning_elevators returned an unknown elevator ID"
        );
    }
}

#[test]
fn iter_repositioning_elevators_empty_after_reposition_completes() {
    let mut sim = SimulationBuilder::new()
        .stops(vec![
            StopConfig {
                id: StopId(0),
                name: "Lobby".into(),
                position: 0.0,
            },
            StopConfig {
                id: StopId(1),
                name: "Mid".into(),
                position: 10.0,
            },
            StopConfig {
                id: StopId(2),
                name: "Top".into(),
                position: 20.0,
            },
        ])
        .elevator(crate::config::ElevatorConfig {
            id: 0,
            name: "A".into(),
            max_speed: Speed::from(2.0),
            acceleration: Accel::from(1.5),
            deceleration: Accel::from(2.0),
            weight_capacity: Weight::from(800.0),
            starting_stop: StopId(2),
            door_open_ticks: 10,
            door_transition_ticks: 5,
            restricted_stops: Vec::new(),
            #[cfg(feature = "energy")]
            energy_profile: None,
            service_mode: None,
            inspection_speed_factor: 0.25,

            bypass_load_up_pct: None,

            bypass_load_down_pct: None,
        })
        .dispatch(EtdDispatch::new())
        .reposition(ReturnToLobby::new(), BuiltinReposition::ReturnToLobby)
        .build()
        .unwrap();

    // Run long enough for reposition to complete (20 units at 2.0 u/s).
    for _ in 0..2000 {
        sim.step();
    }

    assert!(
        sim.iter_repositioning_elevators().next().is_none(),
        "no elevators should be repositioning after arrival at home stop"
    );
}

// ── RiderBuilder ──────────────────────────────────────────────────────────────

#[test]
fn rider_builder_basic_spawn() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .spawn()
        .unwrap();

    let rider = sim.world().rider(rider_id.entity()).unwrap();
    assert_eq!(rider.phase, RiderPhase::Waiting);
}

#[test]
fn rider_builder_custom_weight() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .weight(90.0)
        .spawn()
        .unwrap();

    let rider = sim.world().rider(rider_id.entity()).unwrap();
    assert!(
        (rider.weight.value() - 90.0).abs() < f64::EPSILON,
        "rider weight should be 90.0, got {}",
        rider.weight
    );
}

#[test]
fn rider_builder_with_explicit_group() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    // The default config has one group: GroupId(0).
    let rider_id = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .group(GroupId(0))
        .spawn()
        .unwrap();

    let rider = sim.world().rider(rider_id.entity()).unwrap();
    assert_eq!(rider.phase, RiderPhase::Waiting);
}

#[test]
fn rider_builder_with_patience() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .patience(100)
        .spawn()
        .unwrap();

    let patience = sim.world().patience(rider_id.entity()).unwrap();
    assert_eq!(
        patience.max_wait_ticks(),
        100,
        "patience max_wait_ticks should be 100"
    );
    assert_eq!(patience.waited_ticks(), 0);
}

#[test]
fn rider_builder_with_preferences() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let prefs = Preferences {
        skip_full_elevator: true,
        max_crowding_factor: 0.5,
        abandon_after_ticks: None,
        abandon_on_full: false,
    };

    let rider_id = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .preferences(prefs)
        .spawn()
        .unwrap();

    let stored = sim.world().preferences(rider_id.entity()).unwrap();
    assert!(
        stored.skip_full_elevator(),
        "skip_full_elevator should be true"
    );
    assert!(
        (stored.max_crowding_factor() - 0.5).abs() < f64::EPSILON,
        "max_crowding_factor should be 0.5"
    );
}

#[test]
fn rider_builder_with_access_control() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let stop0 = sim.stop_entity(StopId(0)).unwrap();
    let stop2 = sim.stop_entity(StopId(2)).unwrap();
    let mut allowed = HashSet::new();
    allowed.insert(stop0);
    allowed.insert(stop2);
    let ac = AccessControl::new(allowed.clone());

    let rider_id = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .access_control(ac)
        .spawn()
        .unwrap();

    let stored = sim.world().access_control(rider_id.entity()).unwrap();
    assert!(
        stored.can_access(stop0),
        "rider should have access to stop 0"
    );
    assert!(
        stored.can_access(stop2),
        "rider should have access to stop 2"
    );
}

#[test]
fn rider_builder_invalid_stop_id_returns_stop_not_found() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let result = sim.build_rider(StopId(0), StopId(99));
    assert!(
        matches!(result, Err(SimError::StopNotFound(StopId(99)))),
        "expected StopNotFound(99)"
    );
}

/// `spawn_rider` and `RiderBuilder::spawn` (when no explicit route is
/// provided) reject `origin == destination`. Same-stop spawns produce
/// no hall call so the rider would deadlock Waiting forever (#273).
#[test]
fn spawn_rider_rejects_origin_equals_destination() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let direct = sim.spawn_rider(StopId(0), StopId(0), 70.0);
    assert!(
        matches!(
            direct,
            Err(SimError::InvalidConfig {
                field: "destination",
                ..
            })
        ),
        "spawn_rider with same stop must error, got {direct:?}"
    );

    let builder = sim.build_rider(StopId(1), StopId(1)).unwrap().spawn();
    assert!(
        matches!(
            builder,
            Err(SimError::InvalidConfig {
                field: "destination",
                ..
            })
        ),
        "RiderBuilder::spawn with same stop must error, got {builder:?}"
    );

    // No rider was actually spawned.
    assert_eq!(sim.metrics().total_spawned(), 0);
}

#[test]
fn rider_builder_no_route_when_stops_not_in_same_group() {
    // Build a sim with two separate groups, each with its own stops.
    let mut sim = SimulationBuilder::new()
        .stops(vec![
            StopConfig {
                id: StopId(0),
                name: "A".into(),
                position: 0.0,
            },
            StopConfig {
                id: StopId(1),
                name: "B".into(),
                position: 10.0,
            },
        ])
        .elevator(crate::config::ElevatorConfig {
            id: 0,
            name: "E1".into(),
            max_speed: Speed::from(2.0),
            acceleration: Accel::from(1.5),
            deceleration: Accel::from(2.0),
            weight_capacity: Weight::from(800.0),
            starting_stop: StopId(0),
            door_open_ticks: 10,
            door_transition_ticks: 5,
            restricted_stops: Vec::new(),
            #[cfg(feature = "energy")]
            energy_profile: None,
            service_mode: None,
            inspection_speed_factor: 0.25,

            bypass_load_up_pct: None,

            bypass_load_down_pct: None,
        })
        .build()
        .unwrap();

    // Remove stop 1 from the group so it's unreachable.
    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    // Remove stop 1 from all lines so it's no longer in the group cache.
    sim.remove_stop(stop1).unwrap();

    // Now attempting to build a rider from stop 0 to the removed stop1
    // should fail because stop1 is no longer in the stop_lookup.
    let result = sim.build_rider(StopId(0), StopId(1));
    assert!(
        matches!(result, Err(SimError::StopNotFound(_))),
        "expected StopNotFound after stop removed"
    );
}

#[test]
fn rider_builder_spawn_returns_no_route_when_group_not_serving_both_stops() {
    // Build a sim, then spawn a rider using entity IDs directly against a group
    // that doesn't serve the destination — by using the builder with a bad explicit group.
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    // GroupId(99) does not exist.
    let result = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .group(GroupId(99))
        .spawn();

    assert!(
        matches!(result, Err(SimError::GroupNotFound(GroupId(99)))),
        "expected GroupNotFound(99), got {result:?}"
    );
}

// ── Dispatch re-exports (compilation / import tests) ─────────────────────────

#[test]
fn dispatch_scan_resolves() {
    let _: ScanDispatch = ScanDispatch::new();
}

#[test]
fn dispatch_look_resolves() {
    let _: LookDispatch = LookDispatch::new();
}

#[test]
fn dispatch_etd_resolves() {
    let _: EtdDispatch = EtdDispatch::new();
}

#[test]
fn dispatch_nearest_car_resolves() {
    let _: NearestCarDispatch = NearestCarDispatch::new();
}

// ── Additional edge cases ─────────────────────────────────────────────────────

#[test]
fn remove_elevator_then_riders_on_returns_empty() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();
    let elevator_id = sim.groups()[0].elevator_entities()[0];

    sim.remove_elevator(elevator_id).unwrap();

    // After removal, riders_on should return empty (not panic).
    assert!(sim.riders_on(elevator_id.into()).is_empty());
}

#[test]
fn remove_elevator_then_occupancy_returns_zero() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();
    let elevator_id = sim.groups()[0].elevator_entities()[0];

    sim.remove_elevator(elevator_id).unwrap();

    assert_eq!(sim.occupancy(elevator_id.into()), 0);
}

#[test]
fn drain_events_where_with_all_matching_empties_buffer() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    for _ in 0..5 {
        sim.step();
    }

    // Drain everything.
    let all = sim.drain_events_where(|_| true);
    assert!(!all.is_empty(), "should have some events to drain");

    // Buffer should now be empty.
    let remaining = sim.drain_events();
    assert!(
        remaining.is_empty(),
        "buffer should be empty after all-matching drain"
    );
}

#[test]
fn rider_builder_default_weight_is_75() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .spawn()
        .unwrap();

    let rider = sim.world().rider(rider_id.entity()).unwrap();
    assert!(
        (rider.weight.value() - 75.0).abs() < f64::EPSILON,
        "default weight should be 75.0, got {}",
        rider.weight
    );
}

#[test]
fn rider_builder_no_patience_by_default() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .spawn()
        .unwrap();

    // Without calling .patience(), no Patience component is set.
    assert!(
        sim.world().patience(rider_id.entity()).is_none(),
        "rider should have no Patience component by default"
    );
}

#[test]
fn rider_builder_no_preferences_by_default() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .spawn()
        .unwrap();

    assert!(
        sim.world().preferences(rider_id.entity()).is_none(),
        "rider should have no Preferences component by default"
    );
}

#[test]
fn rider_builder_no_access_control_by_default() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    let rider_id = sim
        .build_rider(StopId(0), StopId(2))
        .unwrap()
        .spawn()
        .unwrap();

    assert!(
        sim.world().access_control(rider_id.entity()).is_none(),
        "rider should have no AccessControl component by default"
    );
}

// ── ElevatorRemoved / StopRemoved events ──────────────────────────────────────

#[test]
fn remove_elevator_emits_elevator_removed_event() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();
    sim.drain_events();

    let elevator_id = sim.groups()[0].elevator_entities()[0];
    sim.remove_elevator(elevator_id).unwrap();

    let events = sim.drain_events();
    let removed = events
        .iter()
        .any(|e| matches!(e, Event::ElevatorRemoved { elevator, .. } if *elevator == elevator_id));
    assert!(removed, "should emit ElevatorRemoved event");
}

#[test]
fn remove_stop_emits_stop_removed_event() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();
    sim.drain_events();

    let stop_eid = sim.stop_entity(StopId(2)).unwrap();
    sim.remove_stop(stop_eid).unwrap();

    let events = sim.drain_events();
    let removed = events
        .iter()
        .any(|e| matches!(e, Event::StopRemoved { stop, .. } if *stop == stop_eid));
    assert!(removed, "should emit StopRemoved event");
}

/// Removing a stop that has resident riders emits `ResidentsAtRemovedStop`.
#[test]
fn remove_stop_emits_warning_for_residents() {
    let config = default_config();
    let mut sim = Simulation::new(&config, scan()).unwrap();

    // Spawn a rider and run until they arrive.
    sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    for _ in 0..2000 {
        sim.step();
    }
    sim.drain_events();

    // Find an arrived rider and settle them at their stop.
    let arrived: Vec<_> = sim
        .world()
        .iter_riders()
        .filter(|(_, r)| r.phase == RiderPhase::Arrived)
        .map(|(eid, _)| eid)
        .collect();
    assert!(!arrived.is_empty(), "should have an arrived rider");

    let rider_eid = arrived[0];
    let rider = sim.world().rider(rider_eid).unwrap();
    let stop = rider.current_stop.unwrap();
    sim.settle_rider(crate::entity::RiderId::from(rider_eid))
        .unwrap();

    // Verify the rider is now Resident.
    let phase = sim.world().rider(rider_eid).unwrap().phase;
    assert_eq!(phase, RiderPhase::Resident, "rider should be Resident");

    // Now remove the stop where the resident is.
    sim.remove_stop(stop).unwrap();
    let events = sim.drain_events();
    let warning = events.iter().find(|e| {
        matches!(
            e,
            Event::ResidentsAtRemovedStop {
                stop: s,
                residents,
                ..
            } if *s == stop && residents.contains(&rider_eid)
        )
    });
    assert!(
        warning.is_some(),
        "should emit ResidentsAtRemovedStop for resident riders"
    );
}
