//! Compressed oops integration tests.
//!
//! Validates the compressed reference encoding end-to-end with simulated heap
//! allocations, mirroring how reference arrays and field maps would use
//! compression to reduce memory footprint on constrained targets.

use jvmrs::compressed_oops::{CompressedOops, OopMode};
use jvmrs::MemoryError;

/// Simulates a packed reference array that stores compressed 16-bit handles
/// instead of raw 32-bit handles.
struct PackedRefArray {
    oops: CompressedOops,
    slots: Vec<u16>,
}

impl PackedRefArray {
    fn new(oops: CompressedOops, capacity: usize) -> Self {
        Self {
            oops,
            slots: Vec::with_capacity(capacity),
        }
    }

    fn push(&mut self, handle: u32) -> Result<(), MemoryError> {
        self.slots.push(self.oops.encode(handle)?);
        Ok(())
    }

    fn get(&self, index: usize) -> u32 {
        self.oops.decode(self.slots[index])
    }

    fn len(&self) -> usize {
        self.slots.len()
    }

    /// Allocated storage size in bytes (2 bytes per compressed slot).
    fn storage_bytes(&self) -> usize {
        self.slots.len() * 2
    }
}

#[test]
fn test_packed_ref_array_round_trip() {
    let oops = CompressedOops::compressed();
    let mut arr = PackedRefArray::new(oops, 8);

    // Simulate heap allocations
    for handle in 1..=8u32 {
        arr.push(handle).unwrap();
    }

    assert_eq!(arr.len(), 8);
    for (i, expected) in (1..=8u32).enumerate() {
        assert_eq!(arr.get(i), expected);
    }
}

#[test]
fn test_packed_ref_array_saves_memory() {
    let oops = CompressedOops::compressed();
    let mut arr = PackedRefArray::new(oops, 1000);
    for handle in 0..1000u32 {
        arr.push(handle).unwrap();
    }

    // 1000 references * 2 bytes = 2000 bytes compressed
    assert_eq!(arr.storage_bytes(), 2000);

    // Uncompressed would be 4000 bytes -> 50% reduction
    let savings = oops.savings(1000);
    assert_eq!(savings, 2000);
    assert_eq!(savings as f64 / 4000.0, 0.5);
}

#[test]
fn test_overflow_rejects_invalid_handle() {
    let oops = CompressedOops::compressed();
    let mut arr = PackedRefArray::new(oops, 4);

    // Handles within range succeed
    arr.push(65535).unwrap();

    // Out-of-range handle fails
    assert!(matches!(
        arr.push(65536),
        Err(MemoryError::CompressedOopsOverflow(65536))
    ));
    assert_eq!(arr.len(), 1); // Failed push did not advance
}

#[test]
fn test_mode_switch_changes_capacity() {
    let standard = CompressedOops::standard();
    let compressed = CompressedOops::compressed();

    assert_eq!(standard.max_handle(), u32::MAX);
    assert_eq!(compressed.max_handle(), 65535);

    // Standard mode encodes anything (low 16 bits)
    assert!(standard.encode(0xFFFF_FFFF).is_ok());
    // Compressed mode rejects anything beyond 0xFFFF
    assert!(compressed.encode(0xFFFF_FFFF).is_err());
}

#[test]
fn test_aligned_compression_with_shift() {
    // Simulate 8-byte aligned heap slots
    let oops = CompressedOops::compressed_with(0, 3);
    assert_eq!(oops.max_handle(), 0xFFFF * 8);

    let mut arr = PackedRefArray::new(oops, 4);
    let aligned_handles = [0u32, 8, 16, 24];
    for &h in &aligned_handles {
        arr.push(h).unwrap();
    }

    for (i, &expected) in aligned_handles.iter().enumerate() {
        assert_eq!(arr.get(i), expected);
    }
}

#[test]
fn test_bulk_decode_after_encode() {
    let oops = CompressedOops::compressed();
    let original: Vec<u32> = (0..=65535u32).step_by(257).collect();

    let encoded = oops.encode_slice(&original).unwrap();
    let decoded = oops.decode_slice(&encoded);

    assert_eq!(decoded, original);
    // Each encoded value fits in 2 bytes
    assert_eq!(encoded.iter().map(|v| *v as usize).sum::<usize>() > 0, true);
}

#[test]
fn test_default_is_standard() {
    let oops = CompressedOops::default();
    assert_eq!(oops.mode, OopMode::Standard);
    assert!(!oops.is_compressed());
}

#[test]
fn test_compressed_mode_predicate() {
    let standard = CompressedOops::standard();
    let compressed = CompressedOops::compressed();
    let with_shift = CompressedOops::compressed_with(0x1000, 2);

    assert!(!standard.is_compressed());
    assert!(compressed.is_compressed());
    assert!(with_shift.is_compressed());
}

#[test]
fn test_compressed_field_map_simulation() {
    // Simulate an object field map that stores reference fields as compressed
    // handles to reduce per-object header size on embedded targets.
    let oops = CompressedOops::compressed();

    let fields = ["next", "prev", "parent", "child"];
    let raw_values = [10u32, 11, 12, 13];

    let packed: Vec<((&str, u16))> = fields
        .iter()
        .zip(raw_values.iter())
        .map(|(k, &v)| (*k, oops.encode(v).unwrap()))
        .collect();

    // Decode back
    for (i, (_, encoded)) in packed.iter().enumerate() {
        assert_eq!(oops.decode(*encoded), raw_values[i]);
    }
}

#[test]
fn test_compressed_savings_zero_in_standard_mode() {
    let oops = CompressedOops::standard();
    assert_eq!(oops.savings_per_slot(), 0);
    assert_eq!(oops.savings(100_000), 0);
}
