use crate::components::RiderPhase;
use crate::components::{Accel, Route, Speed, Weight};
use crate::config::{
    BuildingConfig, ElevatorConfig, PassengerSpawnConfig, SimConfig, SimulationParams,
};
use crate::dispatch::scan::ScanDispatch;
use crate::entity::RiderId;
use crate::error::SimError;
use crate::events::{Event, RouteInvalidReason};
use crate::ids::GroupId;
use crate::sim::Simulation;
use crate::stop::{StopConfig, StopId};

fn three_stop_config() -> SimConfig {
    SimConfig {
        schema_version: crate::config::CURRENT_CONFIG_SCHEMA_VERSION,
        building: BuildingConfig {
            name: "Reroute".into(),
            stops: vec![
                StopConfig {
                    id: StopId(0),
                    name: "A".into(),
                    position: 0.0,
                },
                StopConfig {
                    id: StopId(1),
                    name: "B".into(),
                    position: 10.0,
                },
                StopConfig {
                    id: StopId(2),
                    name: "C".into(),
                    position: 20.0,
                },
            ],
            lines: None,
            groups: None,
        },
        elevators: vec![ElevatorConfig {
            id: 0,
            name: "E0".into(),
            max_speed: Speed::from(5.0),
            acceleration: Accel::from(3.0),
            deceleration: Accel::from(3.0),
            weight_capacity: Weight::from(800.0),
            starting_stop: StopId(0),
            door_open_ticks: 5,
            door_transition_ticks: 3,
            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,
        }],
        simulation: SimulationParams {
            ticks_per_second: 60.0,
        },
        passenger_spawning: PassengerSpawnConfig {
            mean_interval_ticks: 120,
            weight_range: (50.0, 100.0),
        },
    }
}

#[test]
fn reroute_changes_rider_destination() {
    let config = three_stop_config();
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

    // Spawn rider from 0 → 2.
    let rider = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();

    // Reroute to stop 1 instead.
    let stop0 = sim.stop_entity(StopId(0)).unwrap();
    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    sim.reroute(rider, Route::direct(stop0, stop1, GroupId(0)))
        .unwrap();

    let route = sim.world().route(rider.entity()).unwrap();
    assert_eq!(route.current_destination(), Some(stop1));
}

#[test]
fn disable_stop_reroutes_affected_riders() {
    let config = three_stop_config();
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

    // Spawn rider from 0 → 1.
    let rider = sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();

    // Disable stop 1 — rider should be rerouted to nearest alternative (stop 2).
    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    sim.disable(stop1).unwrap();
    sim.drain_events(); // flush

    let route = sim.world().route(rider.entity()).unwrap();
    let dest = route.current_destination().unwrap();
    // Should have been rerouted to stop 2 (nearest enabled alternative).
    let stop2 = sim.stop_entity(StopId(2)).unwrap();
    assert_eq!(dest, stop2);
}

#[test]
fn disable_stop_emits_route_invalidated_event() {
    let config = three_stop_config();
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

    sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();

    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    sim.disable(stop1).unwrap();

    let events = sim.drain_events();
    let invalidated: Vec<_> = events
        .iter()
        .filter(|e| matches!(e, Event::RouteInvalidated { .. }))
        .collect();

    assert_eq!(invalidated.len(), 1);
    if let Event::RouteInvalidated { reason, .. } = invalidated[0] {
        assert_eq!(*reason, RouteInvalidReason::StopDisabled);
    }
}

#[test]
fn disable_only_stop_causes_abandonment() {
    // Config with only 2 stops. Disable the destination — no alternative.
    let config = SimConfig {
        schema_version: crate::config::CURRENT_CONFIG_SCHEMA_VERSION,
        building: BuildingConfig {
            name: "Two".into(),
            stops: vec![
                StopConfig {
                    id: StopId(0),
                    name: "A".into(),
                    position: 0.0,
                },
                StopConfig {
                    id: StopId(1),
                    name: "B".into(),
                    position: 10.0,
                },
            ],
            lines: None,
            groups: None,
        },
        elevators: vec![ElevatorConfig {
            id: 0,
            name: "E0".into(),
            max_speed: Speed::from(5.0),
            acceleration: Accel::from(3.0),
            deceleration: Accel::from(3.0),
            weight_capacity: Weight::from(800.0),
            starting_stop: StopId(0),
            door_open_ticks: 5,
            door_transition_ticks: 3,
            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,
        }],
        simulation: SimulationParams {
            ticks_per_second: 60.0,
        },
        passenger_spawning: PassengerSpawnConfig {
            mean_interval_ticks: 120,
            weight_range: (50.0, 100.0),
        },
    };

    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
    let rider = sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();

    // Disable the only other stop — no alternative available.
    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    sim.disable(stop1).unwrap();

    // Rider should have been abandoned.
    let r = sim.world().rider(rider.entity()).unwrap();
    assert_eq!(r.phase, RiderPhase::Abandoned);

    // RouteInvalidated forwards the trigger reason (StopDisabled for
    // disable, StopRemoved for remove_stop). The "no alternative found"
    // signal is implicit in the accompanying RiderAbandoned event.
    let events = sim.drain_events();
    let invalidated_count = events
        .iter()
        .filter(|e| {
            matches!(
                e,
                Event::RouteInvalidated {
                    reason: RouteInvalidReason::StopDisabled,
                    ..
                }
            )
        })
        .count();
    assert_eq!(invalidated_count, 1);
    let abandoned_count = events
        .iter()
        .filter(|e| matches!(e, Event::RiderAbandoned { .. }))
        .count();
    assert_eq!(
        abandoned_count, 1,
        "RiderAbandoned conveys 'no alternative'"
    );

    // `invalidate_routes_for_stop`'s abandon path is the fourth
    // rider-abandonment site in the codebase; like the two in
    // `advance_transient` it must scrub the rider from every hall call
    // so `pending_riders.is_empty()` stays accurate for mode detection
    // and car-call cleanup.
    let origin = sim.stop_entity(StopId(0)).unwrap();
    let up = sim
        .world()
        .hall_call(origin, crate::components::CallDirection::Up);
    if let Some(call) = up {
        assert!(
            !call.pending_riders.contains(&rider.entity()),
            "stop-disable abandonment must scrub pending_riders"
        );
    }
}

#[test]
fn set_rider_route_replaces_route() {
    use crate::components::{Route, RouteLeg, TransportMode};
    use crate::ids::GroupId;

    let config = three_stop_config();
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

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

    let stop0 = sim.stop_entity(StopId(0)).unwrap();
    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    let stop2 = sim.stop_entity(StopId(2)).unwrap();

    // Set a multi-leg route: 0→1, then 1→2.
    let route = Route {
        legs: vec![
            RouteLeg {
                from: stop0,
                to: stop1,
                via: TransportMode::Group(GroupId(0)),
            },
            RouteLeg {
                from: stop1,
                to: stop2,
                via: TransportMode::Group(GroupId(0)),
            },
        ],
        current_leg: 0,
    };
    sim.reroute(rider, route).unwrap();

    let r = sim.world().route(rider.entity()).unwrap();
    assert_eq!(r.legs.len(), 2);
    assert_eq!(r.current_destination(), Some(stop1));
}

#[test]
fn reroute_rejects_non_waiting_rider() {
    let config = three_stop_config();
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

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

    // Advance until rider is boarding or riding.
    for _ in 0..500 {
        sim.step();
        let phase = sim.world().rider(rider.entity()).unwrap().phase;
        if matches!(phase, RiderPhase::Riding(_) | RiderPhase::Arrived) {
            break;
        }
    }

    let stop0 = sim.stop_entity(StopId(0)).unwrap();
    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    let result = sim.reroute(rider, Route::direct(stop0, stop1, GroupId(0)));

    // Should fail if rider is not Waiting.
    let phase = sim.world().rider(rider.entity()).unwrap().phase;
    if phase != RiderPhase::Waiting {
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            SimError::WrongRiderPhase { .. }
        ));
    }
}

#[test]
fn reroute_nonexistent_rider_returns_error() {
    let config = three_stop_config();
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

    let stop0 = sim.stop_entity(StopId(0)).unwrap();
    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    // Use a stop entity as a fake rider — it's a valid EntityId but not a rider.
    let result = sim.reroute(
        RiderId::from(stop1),
        Route::direct(stop0, stop1, GroupId(0)),
    );
    assert!(result.is_err());
}

// ── Stop-removal: Riding-rider rerouting (the deadlock fix) ────────

/// Run the sim until the rider reaches a target phase or `cap` ticks elapse.
fn step_until_phase(
    sim: &mut Simulation,
    rider: RiderId,
    matcher: impl Fn(RiderPhase) -> bool,
    cap: u64,
) -> bool {
    for _ in 0..cap {
        sim.step();
        let phase = sim.world().rider(rider.entity()).unwrap().phase;
        if matcher(phase) {
            return true;
        }
    }
    false
}

#[test]
fn remove_stop_with_riding_passenger_reroutes() {
    let config = three_stop_config();
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

    // Rider 0 → 2; advance until they're Riding.
    let rider = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    let made_it = step_until_phase(&mut sim, rider, |p| matches!(p, RiderPhase::Riding(_)), 500);
    assert!(made_it, "rider never boarded within 500 ticks");

    // Now remove the destination stop while they're aboard.
    let stop2 = sim.stop_entity(StopId(2)).unwrap();
    sim.remove_stop(stop2).unwrap();
    sim.drain_events();

    // Rider's route must point at the surviving alternative (stop 1).
    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    let dest = sim
        .world()
        .route(rider.entity())
        .and_then(crate::components::Route::current_destination);
    assert_eq!(dest, Some(stop1), "rerouted destination should be stop 1");

    // Eventually the rider exits at the substitute and never gets stuck.
    let arrived = step_until_phase(&mut sim, rider, |p| matches!(p, RiderPhase::Arrived), 2_000);
    assert!(
        arrived,
        "rider was not delivered to substitute stop within 2000 ticks (deadlock?)"
    );
}

#[test]
fn remove_stop_with_riding_passenger_emits_stop_removed_event() {
    let config = three_stop_config();
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

    let rider = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    step_until_phase(&mut sim, rider, |p| matches!(p, RiderPhase::Riding(_)), 500);
    sim.drain_events();

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

    let count = sim
        .drain_events()
        .into_iter()
        .filter(|e| {
            matches!(
                e,
                Event::RouteInvalidated {
                    reason: RouteInvalidReason::StopRemoved,
                    ..
                }
            )
        })
        .count();
    assert_eq!(count, 1, "expected exactly one StopRemoved event");
}

#[test]
fn remove_only_destination_with_riding_passenger_returns_to_origin() {
    // 2-stop config; rider en route from stop 0 to stop 1; remove stop 1.
    // Stop 0 is the only enabled alternative, so the rider is rerouted
    // back to the origin and the car turns around to deliver them. This
    // is the graceful "demolish destination floor" outcome — no deadlock.
    let config = SimConfig {
        schema_version: crate::config::CURRENT_CONFIG_SCHEMA_VERSION,
        building: BuildingConfig {
            name: "Two".into(),
            stops: vec![
                StopConfig {
                    id: StopId(0),
                    name: "A".into(),
                    position: 0.0,
                },
                StopConfig {
                    id: StopId(1),
                    name: "B".into(),
                    position: 10.0,
                },
            ],
            lines: None,
            groups: None,
        },
        elevators: vec![ElevatorConfig {
            id: 0,
            name: "E0".into(),
            max_speed: Speed::from(5.0),
            acceleration: Accel::from(3.0),
            deceleration: Accel::from(3.0),
            weight_capacity: Weight::from(800.0),
            starting_stop: StopId(0),
            door_open_ticks: 5,
            door_transition_ticks: 3,
            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,
        }],
        simulation: SimulationParams {
            ticks_per_second: 60.0,
        },
        passenger_spawning: PassengerSpawnConfig {
            mean_interval_ticks: 120,
            weight_range: (50.0, 100.0),
        },
    };
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

    let rider = sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
    step_until_phase(&mut sim, rider, |p| matches!(p, RiderPhase::Riding(_)), 500);

    let stop0 = sim.stop_entity(StopId(0)).unwrap();
    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    sim.remove_stop(stop1).unwrap();

    // Route now points at stop 0 (the surviving alternative).
    let dest = sim
        .world()
        .route(rider.entity())
        .and_then(crate::components::Route::current_destination);
    assert_eq!(dest, Some(stop0));

    // Sim eventually delivers them; no deadlock.
    let arrived = step_until_phase(&mut sim, rider, |p| matches!(p, RiderPhase::Arrived), 2_000);
    assert!(
        arrived,
        "rider was not delivered after rerouting to origin (deadlock?)"
    );
}

#[test]
fn remove_stop_without_alternative_emits_stop_removed_not_no_alternative() {
    // Two-stop scenario; rider Waiting at stop 0 bound for stop 1.
    // Removing stop 1 leaves no alternative — the abandon path fires.
    // Reason should be `StopRemoved` (not `NoAlternative`) so consumers
    // can distinguish a permanent removal from a transient disable.
    let config = SimConfig {
        schema_version: crate::config::CURRENT_CONFIG_SCHEMA_VERSION,
        building: BuildingConfig {
            name: "Two".into(),
            stops: vec![
                StopConfig {
                    id: StopId(0),
                    name: "A".into(),
                    position: 0.0,
                },
                StopConfig {
                    id: StopId(1),
                    name: "B".into(),
                    position: 10.0,
                },
            ],
            lines: None,
            groups: None,
        },
        elevators: vec![ElevatorConfig {
            id: 0,
            name: "E0".into(),
            max_speed: Speed::from(5.0),
            acceleration: Accel::from(3.0),
            deceleration: Accel::from(3.0),
            weight_capacity: Weight::from(800.0),
            starting_stop: StopId(0),
            door_open_ticks: 5,
            door_transition_ticks: 3,
            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,
        }],
        simulation: SimulationParams {
            ticks_per_second: 60.0,
        },
        passenger_spawning: PassengerSpawnConfig {
            mean_interval_ticks: 120,
            weight_range: (50.0, 100.0),
        },
    };
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
    sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
    sim.drain_events();

    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    sim.remove_stop(stop1).unwrap();

    let events = sim.drain_events();
    let stop_removed = events
        .iter()
        .filter(|e| {
            matches!(
                e,
                Event::RouteInvalidated {
                    reason: RouteInvalidReason::StopRemoved,
                    ..
                }
            )
        })
        .count();
    assert_eq!(
        stop_removed, 1,
        "abandon path should forward StopRemoved reason, not NoAlternative"
    );
}

#[test]
fn ejecting_rider_decrements_car_load_and_emits_capacity_changed() {
    // 2-stop scenario: rider rides 0 → 1, but the no-alternative branch
    // never fires because stop 0 is a valid alternative. To exercise the
    // ejection path we'd need every group-stop removed, which the current
    // mutation API doesn't easily expose. So instead we verify the
    // reroute path's load semantics: weight is preserved when a rider is
    // re-routed to an alternative (no double-counting), and `target_stop`
    // gets re-primed without phase clobber.
    let config = three_stop_config();
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

    let rider = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
    step_until_phase(&mut sim, rider, |p| matches!(p, RiderPhase::Riding(_)), 500);

    let phase_before_remove = sim.world().rider(rider.entity()).unwrap().phase;
    let RiderPhase::Riding(car_eid) = phase_before_remove else {
        panic!("expected Riding, got {phase_before_remove:?}");
    };
    let load_before = sim.world().elevator(car_eid).unwrap().current_load.value();
    assert!(load_before >= 70.0, "rider weight should be in load");

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

    // Rider was rerouted (not ejected), so load is unchanged.
    let load_after = sim.world().elevator(car_eid).unwrap().current_load.value();
    assert!(
        (load_after - load_before).abs() < 0.001,
        "reroute should not change load: before={load_before}, after={load_after}"
    );
}

#[test]
fn remove_stop_evicts_rider_index_entries() {
    let config = three_stop_config();
    let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();

    // Spawn riders bound for stop 1 (so they wait there as Waiting/Resident).
    let _r0 = sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
    let _r1 = sim.spawn_rider(StopId(2), StopId(1), 70.0).unwrap();
    sim.drain_events();

    let stop1 = sim.stop_entity(StopId(1)).unwrap();
    // Confirm we actually had something at stop 1 before we yank it.
    sim.remove_stop(stop1).unwrap();

    // After removal, no index entries should reference the despawned stop.
    // (Rebuild guarantees this — stop's EntityId is no longer alive.)
    assert_eq!(sim.waiting_count_at(stop1), 0);
    assert_eq!(sim.resident_count_at(stop1), 0);
    assert_eq!(sim.abandoned_count_at(stop1), 0);
}
