//! Spec-compliance tests for FINDING-NEW-03: stall events survive event queue overflow.
//!
//! ARM IHI0070G.b §3.12.2 (Stall fault model), §6.3 (Event queue),
//! §7.3 (Event records).
//!
//! Requirements:
//! - `EventEntry` carries a `stall: bool` field that is `true` for events
//!   generated by streams configured with `FaultMode::Stall`, and `false`
//!   otherwise.
//! - When the event queue is full and a new non-stall event arrives, it MAY
//!   be dropped.
//! - When the event queue is full and a new stall event arrives, the stall
//!   event MUST NOT be dropped; it must be preserved in the queue so that
//!   software can act on the stall (ARM §3.12.2 — stall requires software
//!   intervention; silently losing the event would deadlock the device).
//!
//! FINDING-NEW-03 — tests must be RED (compile error on `e.stall`) before
//! the `stall: bool` field is added to `EventEntry`.

#![allow(clippy::unwrap_used)]
#![allow(clippy::items_after_statements)]
#![allow(missing_docs)]

use smmu::types::{
    AccessType, EventEntry, EventType, FaultMode, PagePermissions, QueueConfig, SMMUConfig,
    SecurityState, StreamConfig, StreamID, IOVA, PA, PASID,
};
use smmu::SMMU;

// ── Helpers ───────────────────────────────────────────────────────────────────

fn sid(n: u32) -> StreamID {
    StreamID::new(n).unwrap()
}

fn pasid(n: u32) -> PASID {
    PASID::new(n).unwrap()
}

fn iova(addr: u64) -> IOVA {
    IOVA::new(addr).unwrap()
}

fn pa(addr: u64) -> PA {
    PA::new(addr).unwrap()
}

/// Build a small SMMU whose event queue capacity is 4.
///
/// Size 4 is accepted as a special overflow-test size by `QueueConfig::validate`
/// (see `src/types/config.rs`).
fn make_overflow_smmu() -> SMMU {
    let config = SMMUConfig::builder()
        .queue_config(
            QueueConfig::builder()
                .event_queue_size(4)
                .build()
                .unwrap(),
        )
        .build()
        .unwrap();
    let smmu = SMMU::with_config(config);
    smmu.enable().unwrap();
    smmu
}

/// Configure a stall-mode Stage-1 stream with one mapped page.
///
/// Returns the stream ID so callers can translate an *unmapped* IOVA to
/// trigger a stall fault event.
fn setup_stall_stream(smmu: &SMMU, stream_n: u32, mapped_iova: u64, mapped_pa: u64) {
    let s = sid(stream_n);
    let config = StreamConfig::builder()
        .stage1_enabled(true)
        .translation_enabled(true)
        .security_enforced(false)
        .fault_mode(FaultMode::Stall)
        .build()
        .unwrap();
    smmu.configure_stream(s, config).unwrap();
    smmu.create_pasid(s, pasid(0)).unwrap();
    smmu.map_page(
        s,
        pasid(0),
        iova(mapped_iova),
        pa(mapped_pa),
        PagePermissions::read_write(),
        SecurityState::NonSecure,
    )
    .unwrap();
}

/// Configure a terminate-mode Stage-1 stream with one mapped page.
fn setup_terminate_stream(smmu: &SMMU, stream_n: u32, mapped_iova: u64, mapped_pa: u64) {
    let s = sid(stream_n);
    let config = StreamConfig::builder()
        .stage1_enabled(true)
        .translation_enabled(true)
        .security_enforced(false)
        .fault_mode(FaultMode::Terminate)
        .build()
        .unwrap();
    smmu.configure_stream(s, config).unwrap();
    smmu.create_pasid(s, pasid(0)).unwrap();
    smmu.map_page(
        s,
        pasid(0),
        iova(mapped_iova),
        pa(mapped_pa),
        PagePermissions::read_write(),
        SecurityState::NonSecure,
    )
    .unwrap();
}

// ── Tests ─────────────────────────────────────────────────────────────────────

/// A fault event produced by a `FaultMode::Stall` stream must carry
/// `stall == true` in the queued `EventEntry`.
///
/// ARM §3.12.2: The SMMU stall model requires software to be notified
/// via the event queue so it can issue `CMD_RESUME` or `CMD_STALL_TERM`.
/// The `stall` field lets queue consumers distinguish stall events from
/// ordinary terminate-fault events without inspecting stream configuration.
#[test]
fn test_stall_event_has_stall_field_true() {
    let smmu = SMMU::new();
    smmu.enable().unwrap();

    // Stream 10 — stall mode; only 0x1000 is mapped, 0x2000 is unmapped.
    setup_stall_stream(&smmu, 10, 0x1000, 0x8000_1000);

    // Translate an unmapped IOVA — must produce a stall fault event.
    let _ = smmu.translate(
        sid(10),
        pasid(0),
        iova(0x2000),
        AccessType::Read,
        SecurityState::NonSecure,
    );

    let events = smmu.get_events();
    assert!(
        !events.is_empty(),
        "expected at least one event after stall-mode fault"
    );

    // FINDING-NEW-03: `EventEntry::stall` does not exist yet — compile error
    // is intentional for TDD RED phase.
    assert!(
        events.iter().any(|e| e.stall),
        "event generated by a FaultMode::Stall stream must have stall == true; \
         events: {events:?}"
    );
}

/// A fault event produced by a `FaultMode::Terminate` stream must carry
/// `stall == false` in the queued `EventEntry`.
///
/// Terminate-mode faults abort the transaction immediately and do not
/// require software intervention via `CMD_RESUME`; marking them as stall
/// events would be incorrect.
#[test]
fn test_non_stall_event_has_stall_field_false() {
    let smmu = SMMU::new();
    smmu.enable().unwrap();

    // Stream 20 — terminate mode; 0x1000 mapped, 0x3000 unmapped.
    setup_terminate_stream(&smmu, 20, 0x1000, 0x9000_1000);

    // Translate an unmapped IOVA — must produce a terminate fault event.
    let _ = smmu.translate(
        sid(20),
        pasid(0),
        iova(0x3000),
        AccessType::Read,
        SecurityState::NonSecure,
    );

    let events = smmu.get_events();
    assert!(
        !events.is_empty(),
        "expected at least one event after terminate-mode fault"
    );

    // FINDING-NEW-03: `EventEntry::stall` does not exist yet — compile error
    // is intentional for TDD RED phase.
    assert!(
        events.iter().all(|e| !e.stall),
        "events from FaultMode::Terminate stream must have stall == false; \
         events: {events:?}"
    );
}

/// When the event queue is full (size 4), a stall event submitted for a
/// `FaultMode::Stall` stream MUST survive in the queue.
///
/// ARM §3.12.2 mandates that stalled transactions must eventually receive
/// `CMD_RESUME` or `CMD_STALL_TERM`.  If the stall event is silently dropped
/// during overflow the device is deadlocked: it waits for a RESUME that
/// software does not know it needs to send.  Therefore the SMMU
/// implementation MUST prioritise stall events over non-stall events when
/// the queue is full.
#[test]
fn test_stall_event_survives_queue_overflow() {
    let smmu = make_overflow_smmu();

    // Fill the 4-slot queue with four dummy non-stall F_TRANSLATION events.
    // submit_event returns Err(EventQueueFull) once capacity is reached for
    // small (< 200) queues.
    for _ in 0..4 {
        smmu.submit_event(EventEntry::new(EventType::FTranslation, 0, 0, 0))
            .unwrap();
    }

    // Confirm queue is at capacity before we attempt the stall event.
    assert_eq!(
        smmu.get_event_queue_size(),
        4,
        "queue must be full before the stall translation"
    );

    // Stream 10 in stall mode — the unmapped translation generates a stall event.
    setup_stall_stream(&smmu, 10, 0x1000, 0x8000_1000);
    let _ = smmu.translate(
        sid(10),
        pasid(0),
        iova(0x2000),
        AccessType::Read,
        SecurityState::NonSecure,
    );

    // BUG-13 fix / ARM §7.4: When the queue is full, stall events go to
    // stall_pending rather than being dropped.  get_events() drains
    // stall_pending into the main queue when space is available ("when the
    // queue is next writable").
    //
    // Since the main queue is full (4/4), get_events() cannot drain yet.
    // Verify via get_pending_stall_count() that the event was preserved.
    let pending_stall_count = smmu.get_pending_stall_count();
    let events = smmu.get_events();
    let immediate_stall_count = events.iter().filter(|e| e.stall).count();

    // The stall event must be preserved: in stall_pending (queue was full) or
    // in the main queue (if space was available).
    assert!(
        pending_stall_count >= 1 || immediate_stall_count >= 1,
        "stall event must survive queue overflow (in stall_pending or event queue); \
         pending_stall_count={pending_stall_count}, immediate_stall_count={immediate_stall_count}, \
         total events={}, event_types={:?}",
        events.len(),
        events.iter().map(|e| e.event_type).collect::<Vec<_>>()
    );

    // BUG-1 fix / ARM §3.5.3/§3.12.2: clear_event_queue() now also clears
    // stall_pending so the two containers stay in sync.  This is correct ARM
    // behavior: a software queue reset must be atomic across all holding buffers.
    //
    // Verify that the stall event was preserved in stall_pending (checked above),
    // and that after clear_event_queue() both the main queue and stall_pending
    // are empty (BUG-1 post-condition).
    smmu.clear_event_queue();
    assert_eq!(
        smmu.get_pending_stall_count(),
        0,
        "BUG-1: after clear_event_queue(), stall_pending must also be empty"
    );
    assert_eq!(
        smmu.get_event_queue_size(),
        0,
        "BUG-1: after clear_event_queue(), main event queue must be empty"
    );
}

/// When the event queue is full (size 4) and an additional *non-stall*
/// event arrives, that event MUST be dropped and the existing queue
/// contents MUST be preserved intact.
///
/// This is the counterpart to `test_stall_event_survives_queue_overflow`:
/// ordinary (non-stall) overflow events can be discarded because the
/// existing GERROR mechanism signals the overflow condition to software
/// without requiring the individual event record to be retained.
#[test]
fn test_non_stall_event_dropped_when_queue_full() {
    let smmu = make_overflow_smmu();

    // Fill the 4-slot queue with dummy events that have stream_id == 42,
    // so we can verify they are not evicted when overflow occurs.
    for _ in 0..4 {
        smmu.submit_event(EventEntry::new(EventType::CBadStreamid, 42, 0, 0))
            .unwrap();
    }

    assert_eq!(
        smmu.get_event_queue_size(),
        4,
        "queue must be full before overflow attempt"
    );

    // Translate an unknown StreamID 99 — produces a C_BAD_STREAMID event
    // (non-stall) that must be dropped because the queue is full.
    let _ = smmu.translate(
        sid(99),
        pasid(0),
        iova(0x1000),
        AccessType::Read,
        SecurityState::NonSecure,
    );

    let events = smmu.get_events();

    // Queue length must not exceed 4 — the overflow event was dropped.
    assert_eq!(
        events.len(),
        4,
        "queue length must remain 4 after non-stall overflow; got {}",
        events.len()
    );

    // The original events (stream_id == 42) must still be present — they
    // must NOT have been evicted to make room for the dropped event.
    assert!(
        events.iter().any(|e| e.stream_id == 42),
        "original events with stream_id=42 must not be evicted on non-stall overflow; \
         events: {events:?}"
    );

    // FINDING-NEW-03: `EventEntry::stall` does not exist yet — compile error
    // is intentional for TDD RED phase.
    // Additionally verify that no stall events slipped into the queue.
    assert!(
        events.iter().all(|e| !e.stall),
        "dropped non-stall event must not appear as a stall event; \
         events: {events:?}"
    );
}
