use std::collections::HashSet;

use crate::components::AccessControl;
use crate::error::RejectionReason;
use crate::events::Event;
use crate::stop::StopId;

use super::helpers;

/// Rider rejected when elevator has `restricted_stops` containing the destination.
#[test]
fn rider_rejected_by_elevator_restriction() {
    let mut config = helpers::default_config();
    // Restrict elevator from serving Floor 3 (StopId(2)).
    config.elevators[0].restricted_stops = vec![StopId(2)];

    let mut sim =
        crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");

    // Rider wants to go from Ground to Floor 3 (restricted).
    sim.spawn_rider(StopId(0), StopId(2), 70.0)
        .expect("spawn should succeed");

    let mut all_events = Vec::new();
    for _ in 0..500 {
        sim.step();
        all_events.extend(sim.drain_events());
    }

    let rejected = all_events.iter().any(|e| {
        matches!(
            e,
            Event::RiderRejected {
                reason: RejectionReason::AccessDenied,
                ..
            }
        )
    });
    assert!(rejected, "rider should be rejected with AccessDenied");

    // Rider should NOT have arrived since the only elevator is restricted.
    assert!(
        !helpers::all_riders_arrived(&sim),
        "rider should not arrive when the only elevator is restricted"
    );
}

/// Rider rejected when their `AccessControl` does not include the destination.
#[test]
fn rider_rejected_by_rider_access_control() {
    let config = helpers::default_config();
    let mut sim =
        crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");

    // Spawn rider to Floor 3.
    let rider = sim
        .spawn_rider(StopId(0), StopId(2), 70.0)
        .expect("spawn should succeed");

    // Set rider access to only allow Ground and Floor 2 — NOT Floor 3.
    let stop0 = sim.stop_entity(StopId(0)).expect("stop 0 exists");
    let stop1 = sim.stop_entity(StopId(1)).expect("stop 1 exists");
    sim.set_rider_access(rider.entity(), HashSet::from([stop0, stop1]))
        .expect("set_rider_access should succeed");

    let mut all_events = Vec::new();
    for _ in 0..500 {
        sim.step();
        all_events.extend(sim.drain_events());
    }

    let rejected = all_events.iter().any(|e| {
        matches!(
            e,
            Event::RiderRejected {
                reason: RejectionReason::AccessDenied,
                ..
            }
        )
    });
    assert!(
        rejected,
        "rider should be rejected with AccessDenied due to rider access control"
    );
}

/// Rider boards normally when no access restrictions exist.
#[test]
fn rider_boards_without_restrictions() {
    let config = helpers::default_config();
    let mut sim =
        crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");

    sim.spawn_rider(StopId(0), StopId(2), 70.0)
        .expect("spawn should succeed");

    for _ in 0..2000 {
        sim.step();
        if helpers::all_riders_arrived(&sim) {
            break;
        }
    }

    assert!(
        helpers::all_riders_arrived(&sim),
        "rider should arrive when no restrictions"
    );
}

/// Rider with `AccessControl` listing the destination boards normally.
#[test]
fn rider_boards_when_destination_in_allowed_stops() {
    let config = helpers::default_config();
    let mut sim =
        crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");

    let rider = sim
        .spawn_rider(StopId(0), StopId(2), 70.0)
        .expect("spawn should succeed");

    // Allow all three stops.
    let stop0 = sim.stop_entity(StopId(0)).expect("stop 0 exists");
    let stop1 = sim.stop_entity(StopId(1)).expect("stop 1 exists");
    let stop2 = sim.stop_entity(StopId(2)).expect("stop 2 exists");
    sim.set_rider_access(rider.entity(), HashSet::from([stop0, stop1, stop2]))
        .expect("set_rider_access should succeed");

    for _ in 0..2000 {
        sim.step();
        if helpers::all_riders_arrived(&sim) {
            break;
        }
    }

    assert!(
        helpers::all_riders_arrived(&sim),
        "rider should arrive when destination is in allowed_stops"
    );
}

/// Elevator restriction does not affect riders going to unrestricted stops.
#[test]
fn restriction_does_not_affect_unrestricted_destinations() {
    let mut config = helpers::default_config();
    // Restrict Floor 3 only.
    config.elevators[0].restricted_stops = vec![StopId(2)];

    let mut sim =
        crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");

    // Rider going to Floor 2 (not restricted).
    sim.spawn_rider(StopId(0), StopId(1), 70.0)
        .expect("spawn should succeed");

    for _ in 0..2000 {
        sim.step();
        if helpers::all_riders_arrived(&sim) {
            break;
        }
    }

    assert!(
        helpers::all_riders_arrived(&sim),
        "rider to unrestricted stop should arrive"
    );
}

/// Elevator restriction and rider access control work independently in the same sim.
///
/// Tests each restriction type in isolation (one rider per restriction) to avoid
/// the loading system's single-rejection-slot-per-tick behavior masking failures.
#[test]
fn both_restriction_types_work_in_same_sim() {
    let mut config = helpers::default_config();
    // Elevator restricts Floor 2.
    config.elevators[0].restricted_stops = vec![StopId(1)];

    let mut sim =
        crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");

    // Rider 1: going to Floor 2 (elevator-restricted). Test alone first.
    let rider1 = sim
        .spawn_rider(StopId(0), StopId(1), 70.0)
        .expect("spawn should succeed");

    let mut events_phase1 = Vec::new();
    for _ in 0..200 {
        sim.step();
        events_phase1.extend(sim.drain_events());
    }

    let rider1_rejected = events_phase1.iter().any(|e| {
        matches!(
            e,
            Event::RiderRejected {
                rider,
                reason: RejectionReason::AccessDenied,
                ..
            } if *rider == rider1.entity()
        )
    });
    assert!(
        rider1_rejected,
        "rider1 (elevator-restricted) should be rejected"
    );

    // Despawn rider1 so it doesn't monopolize the rejection slot.
    sim.despawn_rider(rider1).expect("despawn should succeed");

    // Rider 2: going to Floor 3 with access control that doesn't include Floor 3.
    let rider2 = sim
        .spawn_rider(StopId(0), StopId(2), 70.0)
        .expect("spawn should succeed");
    let stop0 = sim.stop_entity(StopId(0)).expect("stop 0 exists");
    let stop1 = sim.stop_entity(StopId(1)).expect("stop 1 exists");
    sim.set_rider_access(rider2.entity(), HashSet::from([stop0, stop1]))
        .expect("set_rider_access should succeed");

    let mut events_phase2 = Vec::new();
    for _ in 0..200 {
        sim.step();
        events_phase2.extend(sim.drain_events());
    }

    let rider2_rejected = events_phase2.iter().any(|e| {
        matches!(
            e,
            Event::RiderRejected {
                rider,
                reason: RejectionReason::AccessDenied,
                ..
            } if *rider == rider2.entity()
        )
    });
    assert!(
        rider2_rejected,
        "rider2 (access-control-restricted) should be rejected"
    );
}

/// `RiderRejected` event carries `AccessDenied` reason with `None` context.
#[test]
fn rejection_event_has_access_denied_reason() {
    let mut config = helpers::default_config();
    config.elevators[0].restricted_stops = vec![StopId(2)];

    let mut sim =
        crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");

    let rider = sim
        .spawn_rider(StopId(0), StopId(2), 70.0)
        .expect("spawn should succeed");

    let mut all_events = Vec::new();
    for _ in 0..500 {
        sim.step();
        all_events.extend(sim.drain_events());
    }

    let rejection = all_events.iter().find(|e| {
        matches!(
            e,
            Event::RiderRejected {
                reason: RejectionReason::AccessDenied,
                ..
            }
        )
    });

    assert!(rejection.is_some(), "should have AccessDenied rejection");
    if let Some(Event::RiderRejected {
        rider: rid,
        reason,
        context,
        ..
    }) = rejection
    {
        assert_eq!(*rid, rider.entity());
        assert_eq!(*reason, RejectionReason::AccessDenied);
        assert!(context.is_none(), "AccessDenied should have no context");
    }
}

/// Elevator is never dispatched (no `ElevatorAssigned` event) to a restricted stop,
/// even when it's the only stop with demand. Verifies the global guard in
/// `systems::dispatch::commit_go_to_stop`.
#[test]
fn elevator_not_dispatched_to_restricted_stop() {
    let mut config = helpers::default_config();
    config.elevators[0].restricted_stops = vec![StopId(2)];

    let mut sim =
        crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");

    sim.spawn_rider(StopId(0), StopId(2), 70.0)
        .expect("spawn should succeed");

    let mut all_events = Vec::new();
    for _ in 0..500 {
        sim.step();
        all_events.extend(sim.drain_events());
    }

    let stop2 = sim.stop_entity(StopId(2)).expect("stop 2 exists");
    let dispatched_to_restricted = all_events
        .iter()
        .any(|e| matches!(e, Event::ElevatorAssigned { stop, .. } if *stop == stop2));
    assert!(
        !dispatched_to_restricted,
        "elevator should never be assigned to a restricted stop"
    );
}

/// `AccessControl` component round-trips through serde.
#[test]
fn access_control_serde_roundtrip() {
    let stop_id = crate::entity::EntityId::default();
    let ac = AccessControl::new(HashSet::from([stop_id]));
    let serialized = ron::to_string(&ac).expect("serialize should succeed");
    let deserialized: AccessControl =
        ron::from_str(&serialized).expect("deserialize should succeed");
    assert!(deserialized.can_access(stop_id));
}

/// `ElevatorConfig` with `restricted_stops` round-trips through RON serde.
#[test]
fn config_restricted_stops_serde_roundtrip() {
    let mut config = helpers::default_config();
    config.elevators[0].restricted_stops = vec![StopId(1), StopId(2)];

    let serialized = ron::to_string(&config).expect("serialize should succeed");
    let deserialized: crate::config::SimConfig =
        ron::from_str(&serialized).expect("deserialize should succeed");
    assert_eq!(deserialized.elevators[0].restricted_stops.len(), 2);
    assert!(
        deserialized.elevators[0]
            .restricted_stops
            .contains(&StopId(1))
    );
    assert!(
        deserialized.elevators[0]
            .restricted_stops
            .contains(&StopId(2))
    );
}

/// `AccessControl::default()` (empty `allowed_stops`) means unrestricted —
/// every `can_access` query returns true. Pre-fix an empty set
/// silently blocked all access, so `set_rider_access(rider,
/// AccessControl::default())` bricked the rider with no warning. (#289)
#[test]
fn access_control_empty_allows_any_stop() {
    use crate::entity::EntityId;

    let ac = AccessControl::default();
    assert!(ac.allowed_stops().is_empty());
    // Any stop entity should be reachable. EntityId::default() is a
    // safe stand-in here because `can_access` doesn't dereference the
    // id, just predicates on set membership.
    assert!(ac.can_access(EntityId::default()));
}

/// Non-empty `allowed_stops` is an allowlist: only listed stops pass.
#[test]
fn access_control_non_empty_is_an_allowlist() {
    use crate::entity::EntityId;

    let allowed = EntityId::default();
    let mut set = HashSet::new();
    set.insert(allowed);
    let ac = AccessControl::new(set);

    assert!(ac.can_access(allowed));
    // Negative case (a different EntityId rejected) is covered by the
    // existing `rider_rejected_by_rider_access_control` integration test.
}

// ── Simulation::run_until_quiet ────────────────────────────────────

/// Returns 0 immediately when the sim has no riders — a fresh sim
/// or one whose riders have all reached a terminal phase is already
/// "quiet" and shouldn't burn ticks waiting.
#[test]
fn run_until_quiet_returns_zero_for_empty_sim() {
    use crate::tests::helpers;

    let mut sim = crate::sim::Simulation::new(&helpers::default_config(), helpers::scan()).unwrap();
    let ticks = sim
        .run_until_quiet(1_000)
        .expect("empty sim is already quiet");
    assert_eq!(ticks, 0);
    assert_eq!(sim.current_tick(), 0);
}

/// Delivers a normal rider and reports how many ticks it took —
/// less than the budget, and positive.
#[test]
fn run_until_quiet_delivers_rider_within_budget() {
    use crate::stop::StopId;
    use crate::tests::helpers;

    let mut sim = crate::sim::Simulation::new(&helpers::default_config(), helpers::scan()).unwrap();
    sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();

    let ticks = sim.run_until_quiet(5_000).expect("rider must deliver");
    assert!(ticks > 0, "delivery took zero ticks?");
    assert!(ticks <= 5_000);
    assert_eq!(sim.metrics().total_delivered(), 1);
}

/// Returns `Err(max_ticks)` when the budget is exhausted before every
/// rider reaches a terminal phase — guards against silent infinite
/// loops when a dispatch stalls. The sim is left in its advanced
/// state so the caller can snapshot it for diagnosis.
#[test]
fn run_until_quiet_returns_err_on_budget_exhaustion() {
    use crate::stop::StopId;
    use crate::tests::helpers;

    let mut sim = crate::sim::Simulation::new(&helpers::default_config(), helpers::scan()).unwrap();
    sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();

    // Two ticks isn't enough to cover a three-stop delivery.
    let err = sim.run_until_quiet(2).unwrap_err();
    assert_eq!(err, 2, "error should carry the exhausted budget");
    assert_eq!(
        sim.current_tick(),
        2,
        "sim state must reflect the steps taken"
    );
}

// ── Simulation::set_dispatch identity resolution ──────────────────

/// `set_dispatch` now consults the strategy's own `builtin_id()` to
/// fill in the snapshot identity, so a caller passing a mismatched
/// `id` argument (or a stale config value) can't silently record
/// the wrong type. The strategy's runtime type always wins for
/// built-ins — mirror of the fix #414 applied to `set_reposition`.
#[test]
fn set_dispatch_prefers_strategy_builtin_id_over_arg() {
    use crate::dispatch::BuiltinStrategy;
    use crate::dispatch::look::LookDispatch;
    use crate::ids::GroupId;
    use crate::sim::Simulation;
    use crate::tests::helpers;

    let mut sim = Simulation::new(&helpers::default_config(), helpers::scan()).unwrap();
    // Caller claims Destination while actually passing LookDispatch.
    // Pre-fix this silently recorded Destination as the snapshot id
    // AND flipped the group's HallCallMode to Destination — the DCS
    // path would then try to service a non-DCS strategy.
    sim.set_dispatch(
        GroupId(0),
        Box::new(LookDispatch::new()),
        BuiltinStrategy::Destination,
    );
    assert_eq!(
        sim.strategy_id(GroupId(0)),
        Some(&BuiltinStrategy::Look),
        "strategy.builtin_id() must override the caller-supplied id \
         when the strategy identifies as a known built-in",
    );
    // The HallCallMode sync keys off the resolved id — with the fix
    // it stays Classic because Look (not Destination) is the real id.
    assert_eq!(
        sim.groups()[0].hall_call_mode(),
        crate::dispatch::HallCallMode::Classic,
        "HallCallMode must follow the resolved built-in, not the \
         stale caller-supplied id",
    );
}
