//! 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
//! [`crate::devices::serial::SMPARK_STATE_GPA`] picks up the
//! right value.
//!
//! Why a separate file? The PL011 scanner has a single global
//! mutable line buffer + global atomics; mixing capture cases
//! in the same file as the rest of the device's tests
//! interleaves state. This file is `#[cfg(test)]`-gated and
//! its tests don't run in parallel with each other (they
//! serialise via `serial_lock` below).

#![cfg(test)]

use std::sync::Mutex;

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

/// Global mutex serialising all tests in this file. The PL011
/// device has a single static line buffer + atomic state; if
/// two tests ran in parallel they'd interleave bytes and
/// corrupt each other's captures.
static TEST_LOCK: Mutex<()> = Mutex::new(());

fn drive(line: &str) {
    let dev = SerialPl011::new();
    // Detection is gated on this flag — production callers
    // enable it for the bake path.
    set_heartbeat_detection(true);
    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 _g = TEST_LOCK.lock().unwrap();
    SMPARK_STATE_GPA.store(0, std::sync::atomic::Ordering::SeqCst);
    drive("[SUPERMACHINE-SMPARK] state_gpa=0x83018000");
    assert_eq!(
        SMPARK_STATE_GPA.load(std::sync::atomic::Ordering::SeqCst),
        0x83018000,
    );
}

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

#[test]
fn captures_max_u64_value() {
    let _g = TEST_LOCK.lock().unwrap();
    SMPARK_STATE_GPA.store(0, std::sync::atomic::Ordering::SeqCst);
    // 16 hex chars — the scanner's max-capture bound.
    drive("[SUPERMACHINE-SMPARK] state_gpa=0xffffffffffffffff");
    assert_eq!(
        SMPARK_STATE_GPA.load(std::sync::atomic::Ordering::SeqCst),
        u64::MAX,
    );
}

#[test]
fn ignores_more_than_16_hex_chars_does_not_overflow() {
    let _g = TEST_LOCK.lock().unwrap();
    SMPARK_STATE_GPA.store(0, std::sync::atomic::Ordering::SeqCst);
    // 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("[SUPERMACHINE-SMPARK] state_gpa=0x1234567890abcdef0");
    // First 16: 0x1234567890abcdef
    assert_eq!(
        SMPARK_STATE_GPA.load(std::sync::atomic::Ordering::SeqCst),
        0x1234567890abcdef,
    );
}

#[test]
fn stops_at_first_non_hex_byte() {
    let _g = TEST_LOCK.lock().unwrap();
    SMPARK_STATE_GPA.store(0, std::sync::atomic::Ordering::SeqCst);
    // After 0xabc, a space terminates the hex scan; trailing
    // log content must not pollute the value.
    drive("[SUPERMACHINE-SMPARK] state_gpa=0xabc trailing-junk");
    assert_eq!(
        SMPARK_STATE_GPA.load(std::sync::atomic::Ordering::SeqCst),
        0xabc,
    );
}

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

#[test]
fn case_sensitive_hex_a_through_f_supported() {
    let _g = TEST_LOCK.lock().unwrap();
    SMPARK_STATE_GPA.store(0, std::sync::atomic::Ordering::SeqCst);
    drive("[SUPERMACHINE-SMPARK] state_gpa=0xABCDEF");
    assert_eq!(
        SMPARK_STATE_GPA.load(std::sync::atomic::Ordering::SeqCst),
        0xabcdef,
    );
}

#[test]
fn second_announcement_overwrites_first() {
    let _g = TEST_LOCK.lock().unwrap();
    SMPARK_STATE_GPA.store(0, std::sync::atomic::Ordering::SeqCst);
    drive("[SUPERMACHINE-SMPARK] state_gpa=0x1000");
    assert_eq!(
        SMPARK_STATE_GPA.load(std::sync::atomic::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("[SUPERMACHINE-SMPARK] state_gpa=0x2000");
    assert_eq!(
        SMPARK_STATE_GPA.load(std::sync::atomic::Ordering::SeqCst),
        0x2000,
    );
}
