use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};

use ax_kspin::SpinRaw as Mutex;

use crate::{
    InterruptTriggerMode,
    config::VMInterruptMode,
    host::irq::{self, IrqSource},
    runtime::{VCpuRef, VMRef},
};

const IOAPIC_GSI_COUNT: usize = 24;
const INVALID_RAW_IRQ: usize = usize::MAX;

const PIT_TIMER_GSI: usize = 0;
const COM1_GSI: usize = 4;
type IoApicForwardingActivator = fn();
type IoApicForwardingActivatorSlot = Mutex<Option<IoApicForwardingActivator>>;
static IOAPIC_IRQ_FORWARDING_ENABLED: AtomicBool = AtomicBool::new(false);
static IOAPIC_IRQ_HOOK_REGISTERED: AtomicBool = AtomicBool::new(false);
static IOAPIC_IRQ_FORWARD_VM_ID: AtomicUsize = AtomicUsize::new(usize::MAX);
static IOAPIC_IRQ_FORWARD_VCPU_ID: AtomicUsize = AtomicUsize::new(usize::MAX);
static IOAPIC_IRQ_PENDING: AtomicUsize = AtomicUsize::new(0);
static IOAPIC_IRQ_PENDING_LEVEL: AtomicUsize = AtomicUsize::new(0);
static IOAPIC_IRQ_MASKED: AtomicUsize = AtomicUsize::new(0);
static IOAPIC_IRQ_ACTIVATED: AtomicUsize = AtomicUsize::new(0);
static IOAPIC_HOST_IRQ_EXPLICIT: AtomicUsize = AtomicUsize::new(0);
static IOAPIC_HOST_IRQ_LEVEL_TRIGGERED: AtomicUsize = AtomicUsize::new(0);
static IOAPIC_IRQ_HANDLES: [AtomicUsize; IOAPIC_GSI_COUNT] =
    [const { AtomicUsize::new(0) }; IOAPIC_GSI_COUNT];
static IOAPIC_HOST_IRQS: [AtomicUsize; IOAPIC_GSI_COUNT] =
    [const { AtomicUsize::new(INVALID_RAW_IRQ) }; IOAPIC_GSI_COUNT];
static IOAPIC_IRQ_ACTIVATORS: [IoApicForwardingActivatorSlot; IOAPIC_GSI_COUNT] =
    [const { Mutex::new(None) }; IOAPIC_GSI_COUNT];

fn should_register_ioapic_gsi_hook(gsi: usize) -> bool {
    gsi < IOAPIC_GSI_COUNT && gsi != PIT_TIMER_GSI
}

fn ioapic_irq_hook_gsis() -> impl Iterator<Item = usize> {
    (0..IOAPIC_GSI_COUNT).filter(|gsi| should_register_ioapic_gsi_hook(*gsi))
}

pub fn register_ioapic_irq_forwarding_route(guest_gsi: usize, host_irq: irq_framework::IrqId) {
    register_ioapic_irq_forwarding_route_with_trigger(
        guest_gsi,
        host_irq,
        InterruptTriggerMode::EdgeTriggered,
    );
}

pub fn register_ioapic_irq_forwarding_route_with_trigger(
    guest_gsi: usize,
    host_irq: irq_framework::IrqId,
    trigger: InterruptTriggerMode,
) {
    if !should_register_ioapic_gsi_hook(guest_gsi) {
        warn!("skip x86 IOAPIC forwarding route for unsupported guest GSI {guest_gsi}");
        return;
    }

    let bit = gsi_bit(guest_gsi);
    IOAPIC_HOST_IRQS[guest_gsi].store(host_irq_to_raw(host_irq), Ordering::Release);
    match trigger {
        InterruptTriggerMode::EdgeTriggered => {
            IOAPIC_HOST_IRQ_LEVEL_TRIGGERED.fetch_and(!bit, Ordering::AcqRel);
        }
        InterruptTriggerMode::LevelTriggered => {
            IOAPIC_HOST_IRQ_LEVEL_TRIGGERED.fetch_or(bit, Ordering::AcqRel);
        }
    }
    IOAPIC_HOST_IRQ_EXPLICIT.fetch_or(bit, Ordering::AcqRel);
    info!(
        "Registered x86 IOAPIC forwarding route: guest GSI {guest_gsi} <- host IRQ {host_irq:?}, \
         trigger {trigger:?}"
    );
}

pub fn register_ioapic_irq_forwarding_activator(
    guest_gsi: usize,
    activator: IoApicForwardingActivator,
) {
    if !should_register_ioapic_gsi_hook(guest_gsi) {
        warn!("skip x86 IOAPIC forwarding activator for unsupported guest GSI {guest_gsi}");
        return;
    }

    *IOAPIC_IRQ_ACTIVATORS[guest_gsi].lock() = Some(activator);
}

pub fn inject_due_pit_irq0(vm: &VMRef, vcpu: &VCpuRef) {
    if vm.interrupt_mode() != VMInterruptMode::Passthrough {
        return;
    }

    let now_ns = crate::host::arceos::monotonic_time_nanos();
    let Ok(devices) = vm.get_devices() else {
        return;
    };
    if !devices.x86_pit_consume_irq0_if_due(now_ns) {
        return;
    }

    let Some(irq) = devices.x86_ioapic_assert_gsi(PIT_TIMER_GSI) else {
        trace!("x86 PIT IRQ0 due but vIOAPIC GSI0 is not ready");
        return;
    };

    vcpu.inject_interrupt_with_trigger(
        irq.vector as _,
        if irq.level_triggered {
            InterruptTriggerMode::LevelTriggered
        } else {
            InterruptTriggerMode::EdgeTriggered
        },
    )
    .unwrap();
}

pub fn inject_pending_serial_irq(vm: &VMRef, vcpu: &VCpuRef) {
    if vm.interrupt_mode() != VMInterruptMode::Passthrough {
        return;
    }

    let Ok(devices) = vm.get_devices() else {
        return;
    };
    if !devices.x86_serial_poll_irq() {
        return;
    }

    let Some(irq) = devices.x86_ioapic_assert_gsi(COM1_GSI) else {
        trace!("x86 COM1 RX pending but vIOAPIC GSI4 is not ready");
        return;
    };

    trace!("Injecting x86 COM1 RX IRQ vector {:#x}", irq.vector);
    vcpu.inject_interrupt_with_trigger(
        irq.vector as _,
        if irq.level_triggered {
            InterruptTriggerMode::LevelTriggered
        } else {
            InterruptTriggerMode::EdgeTriggered
        },
    )
    .unwrap();
}

pub fn inject_pending_ioapic_irq_after_eoi(vm: &VMRef, vcpu: &VCpuRef, vector: u8) {
    if vm.interrupt_mode() != VMInterruptMode::Passthrough {
        return;
    }

    let Ok(devices) = vm.get_devices() else {
        return;
    };
    let Some(eoi) = devices.x86_ioapic_end_of_interrupt(vector) else {
        return;
    };
    let pending = eoi.pending;
    if should_rearm_forwarded_host_gsi_after_eoi(pending) {
        unmask_forwarded_host_gsi(eoi.gsi);
    }

    let Some(irq) = pending else {
        return;
    };

    trace!(
        "Injecting pending x86 IOAPIC level IRQ vector {:#x} after EOI {vector:#x}",
        irq.vector
    );
    vcpu.inject_interrupt_with_trigger(
        irq.vector as _,
        if irq.level_triggered {
            InterruptTriggerMode::LevelTriggered
        } else {
            InterruptTriggerMode::EdgeTriggered
        },
    )
    .unwrap();
}

fn should_rearm_forwarded_host_gsi_after_eoi(pending: Option<x86_vlapic::IoApicInterrupt>) -> bool {
    !pending.is_some_and(|irq| irq.level_triggered)
}

pub fn drain_pending_ioapic_irqs(vm: &VMRef, vcpu: &VCpuRef) {
    if !IOAPIC_IRQ_HOOK_REGISTERED.load(Ordering::Acquire) {
        return;
    }

    if IOAPIC_IRQ_FORWARD_VM_ID.load(Ordering::Acquire) != vm.id()
        || IOAPIC_IRQ_FORWARD_VCPU_ID.load(Ordering::Acquire) != vcpu.id()
    {
        return;
    }

    let pending = IOAPIC_IRQ_PENDING.swap(0, Ordering::AcqRel);
    if pending == 0 {
        return;
    }
    let pending_level = IOAPIC_IRQ_PENDING_LEVEL.fetch_and(!pending, Ordering::AcqRel) & pending;

    let mut retry_pending = 0;
    let mut retry_level_pending = 0;
    for gsi in 0..IOAPIC_GSI_COUNT {
        let bit = 1usize << gsi;
        if pending & bit != 0 {
            let level_triggered = pending_level & bit != 0;
            if forward_passthrough_gsi(vm, vcpu, gsi, level_triggered) {
                if !level_triggered {
                    unmask_forwarded_host_gsi(gsi);
                }
            } else {
                retry_pending |= bit;
                retry_level_pending |= pending_level & bit;
            }
        }
    }

    if retry_pending != 0 {
        IOAPIC_IRQ_PENDING.fetch_or(retry_pending, Ordering::AcqRel);
        IOAPIC_IRQ_PENDING_LEVEL.fetch_or(retry_level_pending, Ordering::AcqRel);
    }
}

pub fn enable_ioapic_irq_forwarding(vm: &VMRef, vcpu: &VCpuRef) {
    if vm.interrupt_mode() != VMInterruptMode::Passthrough {
        return;
    }

    IOAPIC_IRQ_FORWARD_VM_ID.store(vm.id(), Ordering::Release);
    IOAPIC_IRQ_FORWARD_VCPU_ID.store(vcpu.id(), Ordering::Release);

    if IOAPIC_IRQ_FORWARDING_ENABLED
        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
        .is_err()
    {
        return;
    }

    let mut registered = 0;
    for gsi in ioapic_irq_hook_gsis() {
        if IOAPIC_IRQ_HANDLES[gsi].load(Ordering::Acquire) != 0 {
            continue;
        }

        match forwarded_host_irq_for_guest_gsi(gsi) {
            Ok(host_irq) => {
                if host_irq_has_explicit_route_for_other_gsi(host_irq, gsi) {
                    trace!(
                        "skip x86 IOAPIC forwarding fallback for guest GSI {gsi}: host IRQ \
                         {host_irq:?} already has an explicit guest route"
                    );
                    continue;
                }

                match irq::request_shared_irq(host_irq, ioapic_irq_forwarding_handler) {
                    Ok(handle) => {
                        IOAPIC_IRQ_HANDLES[gsi].store(handle.id() as usize, Ordering::Release);
                        registered += 1;
                    }
                    Err(err) => {
                        warn!(
                            "failed to request x86 IOAPIC forwarding IRQ action for host GSI \
                             {gsi}: {err:?}"
                        );
                    }
                }
            }
            Err(err) => {
                trace!("skip x86 IOAPIC forwarding hook for guest GSI {gsi}: {err:?}");
            }
        }
    }
    if registered != 0 {
        IOAPIC_IRQ_HOOK_REGISTERED.store(true, Ordering::Release);
    }
    info!(
        "Enabled x86 IOAPIC IRQ forwarding for host GSIs 0..{} excluding PIT GSI {} ({} newly \
         registered)",
        IOAPIC_GSI_COUNT - 1,
        PIT_TIMER_GSI,
        registered
    );
    activate_ready_ioapic_forwarding_routes(vm);
}

pub fn activate_ready_ioapic_forwarding_routes(vm: &VMRef) {
    if vm.interrupt_mode() != VMInterruptMode::Passthrough {
        return;
    }

    for gsi in ioapic_irq_hook_gsis() {
        let activator = *IOAPIC_IRQ_ACTIVATORS[gsi].lock();
        if activator.is_none() {
            continue;
        }

        if IOAPIC_IRQ_ACTIVATED.load(Ordering::Acquire) & gsi_bit(gsi) != 0 {
            continue;
        }

        let Ok(devices) = vm.get_devices() else {
            return;
        };
        if devices.x86_ioapic_vector_for_gsi(gsi).is_none() {
            continue;
        }

        if IOAPIC_IRQ_ACTIVATED.fetch_or(gsi_bit(gsi), Ordering::AcqRel) & gsi_bit(gsi) != 0 {
            continue;
        }

        if let Some(activator) = activator {
            activate_forwarded_ioapic_gsi(gsi, activator);
        }
    }
}

fn activate_forwarded_ioapic_gsi(gsi: usize, activator: IoApicForwardingActivator) {
    let was_masked = clear_forwarded_ioapic_gsi_state(gsi);
    activator();
    if was_masked {
        set_forwarded_host_gsi_enabled(gsi, true);
    }
}

fn clear_forwarded_ioapic_gsi_state(gsi: usize) -> bool {
    if gsi >= IOAPIC_GSI_COUNT {
        return false;
    }

    let bit = gsi_bit(gsi);
    IOAPIC_IRQ_PENDING.fetch_and(!bit, Ordering::AcqRel);
    IOAPIC_IRQ_PENDING_LEVEL.fetch_and(!bit, Ordering::AcqRel);
    IOAPIC_IRQ_MASKED.fetch_and(!bit, Ordering::AcqRel) & bit != 0
}

#[cfg(test)]
fn activate_ready_ioapic_forwarding_route_for_test(guest_gsi: usize, route_ready: bool) {
    if !should_register_ioapic_gsi_hook(guest_gsi) {
        return;
    }

    if IOAPIC_IRQ_ACTIVATED.load(Ordering::Acquire) & gsi_bit(guest_gsi) != 0 {
        return;
    }

    if !route_ready {
        return;
    }

    if IOAPIC_IRQ_ACTIVATED.fetch_or(gsi_bit(guest_gsi), Ordering::AcqRel) & gsi_bit(guest_gsi) != 0
    {
        return;
    }

    let activator = *IOAPIC_IRQ_ACTIVATORS[guest_gsi].lock();
    if let Some(activator) = activator {
        activate_forwarded_ioapic_gsi(guest_gsi, activator);
    }
}

#[cfg(test)]
fn mark_forwarded_ioapic_gsi_state_for_test(guest_gsi: usize) {
    if should_register_ioapic_gsi_hook(guest_gsi) {
        let bit = gsi_bit(guest_gsi);
        IOAPIC_IRQ_PENDING.fetch_or(bit, Ordering::AcqRel);
        IOAPIC_IRQ_PENDING_LEVEL.fetch_or(bit, Ordering::AcqRel);
        IOAPIC_IRQ_MASKED.fetch_or(bit, Ordering::AcqRel);
    }
}

#[cfg(test)]
fn forwarded_ioapic_gsi_state_for_test(guest_gsi: usize) -> (bool, bool, bool) {
    if !should_register_ioapic_gsi_hook(guest_gsi) {
        return (false, false, false);
    }

    let bit = gsi_bit(guest_gsi);
    (
        IOAPIC_IRQ_PENDING.load(Ordering::Acquire) & bit != 0,
        IOAPIC_IRQ_PENDING_LEVEL.load(Ordering::Acquire) & bit != 0,
        IOAPIC_IRQ_MASKED.load(Ordering::Acquire) & bit != 0,
    )
}

pub fn disable_ioapic_irq_forwarding_for_vm(vm_id: usize) {
    if IOAPIC_IRQ_FORWARD_VM_ID.load(Ordering::Acquire) != vm_id {
        return;
    }

    IOAPIC_IRQ_FORWARD_VM_ID.store(usize::MAX, Ordering::Release);
    IOAPIC_IRQ_FORWARD_VCPU_ID.store(usize::MAX, Ordering::Release);
    IOAPIC_IRQ_PENDING.store(0, Ordering::Release);
    IOAPIC_IRQ_PENDING_LEVEL.store(0, Ordering::Release);
    let masked = IOAPIC_IRQ_MASKED.swap(0, Ordering::AcqRel);
    for gsi in ioapic_irq_hook_gsis() {
        if masked & gsi_bit(gsi) != 0 {
            set_forwarded_host_gsi_enabled(gsi, true);
        }
    }
}

fn forward_passthrough_gsi(
    vm: &VMRef,
    vcpu: &VCpuRef,
    guest_gsi: usize,
    host_level_triggered: bool,
) -> bool {
    if vm.interrupt_mode() != VMInterruptMode::Passthrough {
        return true;
    }

    if guest_gsi >= IOAPIC_GSI_COUNT {
        return true;
    }

    let Ok(devices) = vm.get_devices() else {
        return false;
    };
    let Some(guest_irq) = devices.x86_ioapic_assert_gsi(guest_gsi) else {
        if devices.x86_ioapic_vector_for_gsi(guest_gsi).is_some() {
            trace!(
                "x86 passthrough IRQ for guest GSI {guest_gsi} is deferred by guest vIOAPIC state"
            );
            if !host_level_triggered {
                unmask_forwarded_host_gsi(guest_gsi);
            }
            return true;
        }

        trace!("x86 passthrough IRQ has no injectable guest vIOAPIC route for GSI {guest_gsi}");
        return false;
    };

    vcpu.inject_interrupt_with_trigger(
        guest_irq.vector as _,
        if guest_irq.level_triggered {
            InterruptTriggerMode::LevelTriggered
        } else {
            InterruptTriggerMode::EdgeTriggered
        },
    )
    .unwrap();
    true
}

fn gsi_bit(gsi: usize) -> usize {
    1usize << gsi
}

fn host_irq_to_raw(irq: irq::IrqId) -> usize {
    (usize::from(irq.domain.0) << 32) | irq.hwirq.0 as usize
}

fn forwarded_host_irq_for_guest_gsi(guest_gsi: usize) -> Result<irq::IrqId, irq::IrqError> {
    let raw = IOAPIC_HOST_IRQS[guest_gsi].load(Ordering::Acquire);
    if raw != INVALID_RAW_IRQ {
        return Ok(raw_to_host_irq(raw));
    }

    let source = IrqSource::AcpiGsi(guest_gsi as u32);
    let host_irq = irq::resolve_irq_source(source)?;
    IOAPIC_HOST_IRQS[guest_gsi].store(host_irq_to_raw(host_irq), Ordering::Release);
    Ok(host_irq)
}

fn host_irq_has_explicit_route_for_other_gsi(host_irq: irq::IrqId, guest_gsi: usize) -> bool {
    let raw = host_irq_to_raw(host_irq);
    let explicit = IOAPIC_HOST_IRQ_EXPLICIT.load(Ordering::Acquire);
    ioapic_irq_hook_gsis()
        .filter(|gsi| *gsi != guest_gsi && explicit & gsi_bit(*gsi) != 0)
        .any(|gsi| IOAPIC_HOST_IRQS[gsi].load(Ordering::Acquire) == raw)
}

fn raw_to_host_irq(raw: usize) -> irq::IrqId {
    irq::make_irq_id((raw >> 32) as u16, raw as u32)
}

fn set_forwarded_host_gsi_enabled(gsi: usize, enabled: bool) {
    let raw = IOAPIC_HOST_IRQS
        .get(gsi)
        .map(|irq| irq.load(Ordering::Acquire))
        .unwrap_or(INVALID_RAW_IRQ);
    if raw == INVALID_RAW_IRQ {
        return;
    }
    let irq = raw_to_host_irq(raw);
    if let Err(err) = irq::set_host_irq_enable(irq, enabled) {
        warn!(
            "failed to set forwarded IOAPIC GSI {gsi} host IRQ {irq:?} enabled={enabled}: {err:?}"
        );
    }
}

fn mask_forwarded_host_gsi(gsi: usize) -> bool {
    let bit = gsi_bit(gsi);
    if IOAPIC_IRQ_MASKED.fetch_or(bit, Ordering::AcqRel) & bit != 0 {
        return true;
    }

    let raw = IOAPIC_HOST_IRQS
        .get(gsi)
        .map(|irq| irq.load(Ordering::Acquire))
        .unwrap_or(INVALID_RAW_IRQ);
    if raw == INVALID_RAW_IRQ {
        IOAPIC_IRQ_MASKED.fetch_and(!bit, Ordering::AcqRel);
        return false;
    }

    let irq = raw_to_host_irq(raw);
    if let Err(err) = irq::set_host_irq_enable(irq, false) {
        IOAPIC_IRQ_MASKED.fetch_and(!bit, Ordering::AcqRel);
        warn!("failed to mask forwarded IOAPIC GSI {gsi} host IRQ {irq:?}: {err:?}");
        return false;
    }
    true
}

fn unmask_forwarded_host_gsi(gsi: usize) {
    if gsi >= IOAPIC_GSI_COUNT {
        return;
    }
    let bit = gsi_bit(gsi);
    if IOAPIC_IRQ_MASKED.load(Ordering::Acquire) & bit == 0 {
        return;
    }

    let raw = IOAPIC_HOST_IRQS
        .get(gsi)
        .map(|irq| irq.load(Ordering::Acquire))
        .unwrap_or(INVALID_RAW_IRQ);
    if raw == INVALID_RAW_IRQ {
        return;
    }

    let irq = raw_to_host_irq(raw);
    if let Err(err) = irq::set_host_irq_enable(irq, true) {
        warn!("failed to unmask forwarded IOAPIC GSI {gsi} host IRQ {irq:?}: {err:?}");
        return;
    }
    IOAPIC_IRQ_MASKED.fetch_and(!bit, Ordering::AcqRel);
}

fn ioapic_irq_forwarding_handler(ctx: irq::IrqContext) -> irq::IrqReturn {
    let Some(gsi) = guest_gsi_for_host_irq(ctx.irq) else {
        return irq::IrqReturn::Unhandled;
    };

    if IOAPIC_IRQ_FORWARD_VM_ID.load(Ordering::Acquire) == usize::MAX
        || IOAPIC_IRQ_FORWARD_VCPU_ID.load(Ordering::Acquire) == usize::MAX
    {
        return irq::IrqReturn::Unhandled;
    }

    let bit = gsi_bit(gsi);
    if !mask_forwarded_host_gsi(gsi) {
        return irq::IrqReturn::Unhandled;
    }
    let level_triggered = is_level_triggered_forwarded_host_gsi(gsi);
    if level_triggered {
        IOAPIC_IRQ_PENDING_LEVEL.fetch_or(bit, Ordering::AcqRel);
    }
    IOAPIC_IRQ_PENDING.fetch_or(bit, Ordering::AcqRel);
    irq::IrqReturn::Handled
}

fn is_level_triggered_forwarded_host_gsi(gsi: usize) -> bool {
    gsi < IOAPIC_GSI_COUNT
        && IOAPIC_HOST_IRQ_LEVEL_TRIGGERED.load(Ordering::Acquire) & gsi_bit(gsi) != 0
}

fn guest_gsi_for_host_irq(host_irq: irq::IrqId) -> Option<usize> {
    let raw = host_irq_to_raw(host_irq);
    let explicit = IOAPIC_HOST_IRQ_EXPLICIT.load(Ordering::Acquire);
    if let Some(gsi) = ioapic_irq_hook_gsis()
        .filter(|gsi| explicit & gsi_bit(*gsi) != 0)
        .find(|gsi| IOAPIC_HOST_IRQS[*gsi].load(Ordering::Acquire) == raw)
    {
        return Some(gsi);
    }

    IOAPIC_HOST_IRQS
        .iter()
        .position(|irq| irq.load(Ordering::Acquire) == raw)
}

#[cfg(test)]
mod tests {
    use core::sync::atomic::{AtomicUsize, Ordering};

    use ax_kspin::SpinRaw as Mutex;

    use super::{
        COM1_GSI, INVALID_RAW_IRQ, IOAPIC_GSI_COUNT, IOAPIC_HOST_IRQ_EXPLICIT,
        IOAPIC_HOST_IRQ_LEVEL_TRIGGERED, IOAPIC_HOST_IRQS, IOAPIC_IRQ_ACTIVATED,
        IOAPIC_IRQ_ACTIVATORS, IOAPIC_IRQ_MASKED, IOAPIC_IRQ_PENDING, IOAPIC_IRQ_PENDING_LEVEL,
        PIT_TIMER_GSI, activate_ready_ioapic_forwarding_route_for_test,
        clear_forwarded_ioapic_gsi_state, forwarded_ioapic_gsi_state_for_test, gsi_bit,
        guest_gsi_for_host_irq, host_irq_to_raw, ioapic_irq_hook_gsis,
        is_level_triggered_forwarded_host_gsi, mark_forwarded_ioapic_gsi_state_for_test,
        raw_to_host_irq, register_ioapic_irq_forwarding_activator,
        register_ioapic_irq_forwarding_route, register_ioapic_irq_forwarding_route_with_trigger,
        should_rearm_forwarded_host_gsi_after_eoi, should_register_ioapic_gsi_hook,
    };
    use crate::InterruptTriggerMode;

    static ROUTE_TEST_LOCK: Mutex<()> = Mutex::new(());
    static ACTIVATION_COUNT: AtomicUsize = AtomicUsize::new(0);

    fn reset_forwarding_routes() {
        for host_irq in IOAPIC_HOST_IRQS.iter() {
            host_irq.store(INVALID_RAW_IRQ, Ordering::Release);
        }
        IOAPIC_HOST_IRQ_EXPLICIT.store(0, Ordering::Release);
        IOAPIC_HOST_IRQ_LEVEL_TRIGGERED.store(0, Ordering::Release);
        IOAPIC_IRQ_PENDING.store(0, Ordering::Release);
        IOAPIC_IRQ_PENDING_LEVEL.store(0, Ordering::Release);
        IOAPIC_IRQ_MASKED.store(0, Ordering::Release);
        IOAPIC_IRQ_ACTIVATED.store(0, Ordering::Release);
        crate::host::irq::reset_test_irq_enable_state();
        for activator in IOAPIC_IRQ_ACTIVATORS.iter() {
            *activator.lock() = None;
        }
    }

    fn with_clean_forwarding_routes(test: impl FnOnce()) {
        let _guard = ROUTE_TEST_LOCK.lock();
        reset_forwarding_routes();
        test();
    }

    #[test]
    fn pit_gsi_uses_synthetic_injection_not_host_irq_hook() {
        assert!(!should_register_ioapic_gsi_hook(PIT_TIMER_GSI));
    }

    #[test]
    fn passthrough_gsis_still_register_host_irq_hooks() {
        assert!(should_register_ioapic_gsi_hook(COM1_GSI));
        assert!(should_register_ioapic_gsi_hook(18));
        assert!(should_register_ioapic_gsi_hook(IOAPIC_GSI_COUNT - 1));
        assert!(!should_register_ioapic_gsi_hook(IOAPIC_GSI_COUNT));
    }

    #[test]
    fn hook_gsi_iterator_matches_registration_policy() {
        for gsi in 0..=IOAPIC_GSI_COUNT {
            assert_eq!(
                ioapic_irq_hook_gsis().any(|hook| hook == gsi),
                should_register_ioapic_gsi_hook(gsi)
            );
        }
    }

    #[test]
    fn forwarded_gsi_bits_are_stable() {
        assert_eq!(gsi_bit(0), 1);
        assert_eq!(gsi_bit(18), 1usize << 18);
    }

    #[test]
    fn host_irq_storage_preserves_domain_and_hwirq() {
        let irq = crate::host::irq::make_irq_id(2, 18);
        assert_eq!(raw_to_host_irq(host_irq_to_raw(irq)), irq);
    }

    #[test]
    fn explicit_forwarding_route_wins_over_fallback_route() {
        with_clean_forwarding_routes(|| {
            let fallback_guest_gsi = 7;
            let explicit_guest_gsi = 18;
            let host_irq = crate::host::irq::make_irq_id(2, 7);
            IOAPIC_HOST_IRQS[fallback_guest_gsi]
                .store(host_irq_to_raw(host_irq), Ordering::Release);

            register_ioapic_irq_forwarding_route(explicit_guest_gsi, host_irq);

            assert_eq!(guest_gsi_for_host_irq(host_irq), Some(explicit_guest_gsi));
        });
    }

    #[test]
    fn fallback_registration_skips_host_irq_owned_by_explicit_route() {
        with_clean_forwarding_routes(|| {
            let fallback_guest_gsi = 10;
            let explicit_guest_gsi = 18;
            let host_irq = crate::host::irq::make_irq_id(2, 10);
            IOAPIC_HOST_IRQS[fallback_guest_gsi]
                .store(host_irq_to_raw(host_irq), Ordering::Release);

            register_ioapic_irq_forwarding_route(explicit_guest_gsi, host_irq);

            assert!(super::host_irq_has_explicit_route_for_other_gsi(
                host_irq,
                fallback_guest_gsi
            ));
            assert!(!super::host_irq_has_explicit_route_for_other_gsi(
                host_irq,
                explicit_guest_gsi
            ));
        });
    }

    #[test]
    fn forwarding_trigger_mode_comes_from_registered_route_not_gsi_number() {
        with_clean_forwarding_routes(|| {
            let low_level_gsi = COM1_GSI;
            let high_edge_gsi = 18;
            let low_host_irq = crate::host::irq::make_irq_id(2, low_level_gsi as u32);
            let high_host_irq = crate::host::irq::make_irq_id(2, high_edge_gsi as u32);

            register_ioapic_irq_forwarding_route_with_trigger(
                low_level_gsi,
                low_host_irq,
                InterruptTriggerMode::LevelTriggered,
            );
            register_ioapic_irq_forwarding_route_with_trigger(
                high_edge_gsi,
                high_host_irq,
                InterruptTriggerMode::EdgeTriggered,
            );

            assert!(is_level_triggered_forwarded_host_gsi(low_level_gsi));
            assert!(!is_level_triggered_forwarded_host_gsi(high_edge_gsi));
        });
    }

    fn count_activation() {
        ACTIVATION_COUNT.fetch_add(1, Ordering::AcqRel);
    }

    #[test]
    fn forwarding_activator_waits_for_guest_route_and_runs_once() {
        with_clean_forwarding_routes(|| {
            let guest_gsi = 18;
            ACTIVATION_COUNT.store(0, Ordering::Release);
            register_ioapic_irq_forwarding_activator(guest_gsi, count_activation);

            activate_ready_ioapic_forwarding_route_for_test(guest_gsi, false);
            assert_eq!(ACTIVATION_COUNT.load(Ordering::Acquire), 0);

            activate_ready_ioapic_forwarding_route_for_test(guest_gsi, true);
            assert_eq!(ACTIVATION_COUNT.load(Ordering::Acquire), 1);

            activate_ready_ioapic_forwarding_route_for_test(guest_gsi, true);
            assert_eq!(ACTIVATION_COUNT.load(Ordering::Acquire), 1);
        });
    }

    #[test]
    fn forwarding_activator_drops_pre_activation_pending_state() {
        with_clean_forwarding_routes(|| {
            let guest_gsi = 18;
            let host_irq = crate::host::irq::make_irq_id(2, 10);
            ACTIVATION_COUNT.store(0, Ordering::Release);
            register_ioapic_irq_forwarding_route(guest_gsi, host_irq);
            register_ioapic_irq_forwarding_activator(guest_gsi, count_activation);
            mark_forwarded_ioapic_gsi_state_for_test(guest_gsi);

            activate_ready_ioapic_forwarding_route_for_test(guest_gsi, true);

            assert_eq!(ACTIVATION_COUNT.load(Ordering::Acquire), 1);
            assert_eq!(
                forwarded_ioapic_gsi_state_for_test(guest_gsi),
                (false, false, false)
            );
            assert!(crate::host::irq::test_irq_is_enabled(host_irq));
        });
    }

    #[test]
    fn clearing_forwarded_gsi_state_reports_masked_host_line() {
        with_clean_forwarding_routes(|| {
            let guest_gsi = 18;
            mark_forwarded_ioapic_gsi_state_for_test(guest_gsi);

            assert!(clear_forwarded_ioapic_gsi_state(guest_gsi));
            assert_eq!(
                forwarded_ioapic_gsi_state_for_test(guest_gsi),
                (false, false, false)
            );
            assert!(!clear_forwarded_ioapic_gsi_state(guest_gsi));
        });
    }

    #[test]
    fn forwarded_level_intx_stays_masked_when_guest_eoi_has_deferred_pending() {
        let pending = x86_vlapic::IoApicInterrupt {
            vector: 0x51,
            level_triggered: true,
        };

        assert!(!should_rearm_forwarded_host_gsi_after_eoi(Some(pending)));
    }

    #[test]
    fn forwarded_intx_rearms_host_line_when_guest_eoi_has_no_deferred_level() {
        let pending = x86_vlapic::IoApicInterrupt {
            vector: 0x51,
            level_triggered: false,
        };

        assert!(should_rearm_forwarded_host_gsi_after_eoi(None));
        assert!(should_rearm_forwarded_host_gsi_after_eoi(Some(pending)));
    }
}
