//! Unit tests for the smpark `state_gpa` PL011 line-scanner
//! capture. We feed bytes one at a time through the scanner
//! (mimicking PL011 character writes) and assert the per-VM
//! [`crate::devices::serial::SerialState::smpark_state_gpa`] picks up
//! the right value.
//!
//! Since 0.7.56 each test owns a fresh per-VM `SerialState`, so the
//! scanner's line buffer + atomics are no longer shared — these run
//! fully in parallel with no serialising lock.

#![cfg(test)]

use std::sync::atomic::Ordering;
use std::sync::Arc;

use crate::devices::mmio_bus::MmioDevice;
use crate::devices::serial::{SerialPl011, SerialState};

/// Fresh per-test state + device, with marker detection enabled
/// (production callers enable it for the bake path).
fn fresh() -> (Arc<SerialState>, SerialPl011) {
    let state = Arc::new(SerialState::new());
    state.set_heartbeat_detection(true);
    let dev = SerialPl011::new(state.clone());
    (state, dev)
}

fn drive(dev: &SerialPl011, line: &str) {
    for b in line.bytes() {
        dev.write(0x000, b as u64, 1);
    }
    // The scanner only acts on full-line terminators.
    dev.write(0x000, b'\n' as u64, 1);
}

#[test]
fn captures_well_formed_state_gpa_line() {
    let (st, dev) = fresh();
    drive(&dev, "[SUPERMACHINE-SMPARK] state_gpa=0x83018000");
    assert_eq!(st.smpark_state_gpa.load(Ordering::SeqCst), 0x83018000);
}

#[test]
fn captures_with_surrounding_log_noise() {
    let (st, dev) = fresh();
    // Real-world: kernel printk timestamp + init-oci's prefix.
    // The scanner uses `windows().position()` so the marker
    // can sit anywhere in the line.
    drive(
        &dev,
        "[    0.123] init-oci: foo bar [SUPERMACHINE-SMPARK] state_gpa=0xdeadbeef extra trailing",
    );
    assert_eq!(st.smpark_state_gpa.load(Ordering::SeqCst), 0xdeadbeef);
}

#[test]
fn captures_max_u64_value() {
    let (st, dev) = fresh();
    // 16 hex chars — the scanner's max-capture bound.
    drive(&dev, "[SUPERMACHINE-SMPARK] state_gpa=0xffffffffffffffff");
    assert_eq!(st.smpark_state_gpa.load(Ordering::SeqCst), u64::MAX);
}

#[test]
fn ignores_more_than_16_hex_chars_does_not_overflow() {
    let (st, dev) = fresh();
    // 17 hex chars — should be capped at the first 16. If the
    // scanner didn't bound this, the shift+or loop would
    // silently overflow u64.
    drive(&dev, "[SUPERMACHINE-SMPARK] state_gpa=0x1234567890abcdef0");
    // First 16: 0x1234567890abcdef
    assert_eq!(
        st.smpark_state_gpa.load(Ordering::SeqCst),
        0x1234567890abcdef
    );
}

#[test]
fn stops_at_first_non_hex_byte() {
    let (st, dev) = fresh();
    // After 0xabc, a space terminates the hex scan; trailing
    // log content must not pollute the value.
    drive(&dev, "[SUPERMACHINE-SMPARK] state_gpa=0xabc trailing-junk");
    assert_eq!(st.smpark_state_gpa.load(Ordering::SeqCst), 0xabc);
}

#[test]
fn ignores_lines_without_the_prefix() {
    let (st, dev) = fresh();
    // Pre-set to a known value so we can detect spurious writes.
    st.smpark_state_gpa.store(0xabcd1234, Ordering::SeqCst);
    drive(&dev, "[NOT-SMPARK] state_gpa=0xdead");
    drive(&dev, "init-oci: workload-pre-exec");
    drive(&dev, "smpark: loaded; n_cpus=4 state_gpa=0xbeef");
    // None of these match the strict `[SUPERMACHINE-SMPARK] state_gpa=0x` prefix.
    assert_eq!(st.smpark_state_gpa.load(Ordering::SeqCst), 0xabcd1234);
}

#[test]
fn case_sensitive_hex_a_through_f_supported() {
    let (st, dev) = fresh();
    drive(&dev, "[SUPERMACHINE-SMPARK] state_gpa=0xABCDEF");
    assert_eq!(st.smpark_state_gpa.load(Ordering::SeqCst), 0xabcdef);
}

#[test]
fn second_announcement_overwrites_first() {
    let (st, dev) = fresh();
    drive(&dev, "[SUPERMACHINE-SMPARK] state_gpa=0x1000");
    assert_eq!(st.smpark_state_gpa.load(Ordering::SeqCst), 0x1000);
    // A rebake / reload of smpark.ko could in principle re-emit
    // the line with a different GPA. The scanner doesn't gate
    // on "first" — last write wins.
    drive(&dev, "[SUPERMACHINE-SMPARK] state_gpa=0x2000");
    assert_eq!(st.smpark_state_gpa.load(Ordering::SeqCst), 0x2000);
}
