#![allow(clippy::large_stack_arrays)]
//! Tests for large object interior pointer support.

use rudo_gc::{collect_full, Gc, Trace};

#[cfg(feature = "test-util")]
use rudo_gc::test_util::{clear_test_roots, internal_ptr, register_test_root};

#[cfg(feature = "test-util")]
macro_rules! root {
    ($gc:expr) => {
        register_test_root(internal_ptr(&$gc))
    };
}

#[cfg(not(feature = "test-util"))]
macro_rules! root {
    ($gc:expr) => {};
}

#[cfg(feature = "test-util")]
macro_rules! clear_roots {
    () => {
        clear_test_roots()
    };
}

#[cfg(not(feature = "test-util"))]
macro_rules! clear_roots {
    () => {};
}

#[test]
fn test_large_object_interior_pointer() {
    // 1. Allocate a large object (> 2KB)
    // A vector of 1000 i64 (~8000 bytes) + GcBox overhead will be > 2 pages (8KB)
    let size = 1000;
    let gc = Gc::new(vec![42i64; size]);

    // 2. Get the address of an element deep inside the vector
    // Safety: we know it's a Gc<Vec<i64>>
    let vec_ref = &*gc;
    let middle_element_ptr = std::ptr::from_ref(&vec_ref[size / 2]) as usize;
    let _ = middle_element_ptr;

    // NOTE: Vec stores data on the heap, but Gc<Vec> stores the Vec struct on the GC heap.
    // The actual elements are NOT on the GC heap.
    // To test GC interior pointers, we need a type that HAS the elements inline.
}

#[test]
fn test_large_struct_interior_pointer() {
    #[repr(C)]
    struct LargeStruct {
        data: [u64; 10000], // 80000 bytes, enough for > 64KB page
    }

    unsafe impl Trace for LargeStruct {
        fn trace(&self, _visitor: &mut impl rudo_gc::Visitor) {}
    }

    let gc = Gc::new(LargeStruct {
        data: [0x55; 10000],
    });

    let head_ptr = Gc::as_ptr(&gc) as usize;
    // Use a deeper index to ensure we cross page boundaries even on 64KB pages
    let interior_ptr = (std::ptr::from_ref(&gc.data[8500])) as usize;

    let head_page = head_ptr & rudo_gc::heap::page_mask();
    let interior_page = interior_ptr & rudo_gc::heap::page_mask();

    assert_ne!(
        head_page, interior_page,
        "Interior pointer should be on a different page"
    );

    // head_ptr is the pointer to the value (T) inside GcBox<T>.
    // find_gc_box_from_ptr returns the pointer to GcBox<T>.
    // On 64-bit, the GcBox header is 32 bytes (ref_count, weak_count, drop_fn, trace_fn).
    let expected_gc_box_ptr = head_ptr - 32;

    // Now verify find_gc_box_from_ptr finds it
    rudo_gc::heap::with_heap(|heap| unsafe {
        let found = rudo_gc::heap::find_gc_box_from_ptr(heap, interior_ptr as *const u8);
        assert!(found.is_some(), "Should find GcBox from interior pointer");
        assert_eq!(
            found.unwrap().as_ptr() as usize,
            expected_gc_box_ptr,
            "Should find the CORRECT GcBox"
        );
    });
}

#[test]
fn test_large_object_collection_with_interior_ref() {
    #[repr(C)]
    struct LargeStruct {
        data: [u64; 10000],
    }

    unsafe impl Trace for LargeStruct {
        fn trace(&self, _visitor: &mut impl rudo_gc::Visitor) {}
    }

    let interior_ptr: *const u64;
    {
        clear_roots!();
        let gc = Gc::new(LargeStruct {
            data: [0x77; 10000],
        });
        root!(gc);
        interior_ptr = std::ptr::from_ref(&gc.data[8500]);

        // While original Gc exists, it's alive
        collect_full();
        assert_eq!(unsafe { *interior_ptr }, 0x77);

        // Even with just interior pointer, if we register it, it's alive
        clear_roots!();
        #[cfg(feature = "test-util")]
        register_test_root(interior_ptr.cast::<u8>());
        drop(gc);

        collect_full();

        #[cfg(not(miri))]
        assert_eq!(unsafe { *interior_ptr }, 0x77);

        #[cfg(miri)]
        {
            // On Miri, we avoid dereferencing interior_ptr because the GC tracing
            // might have invalidated the raw pointer stack (Stacked Borrows).
            // Instead, we check metrics to ensure the object survived.
            let metrics = rudo_gc::last_gc_metrics();
            assert!(
                metrics.objects_surviving > 0,
                "Large object should survive via interior pointer"
            );
        }
    }

    // After cleanup, it should be collected
    clear_roots!();
    collect_full();
    // (We can't easily check if it's collected without unsafe/segfault,
    // but we can check statistics or just ensure it doesn't crash)
}

#[inline(never)]
fn force_collect() {
    // Clear stack to remove any residual pointers
    let mut junk = [0usize; 256];
    std::hint::black_box(&mut junk);

    for _ in 0..5 {
        collect_full();
    }
}

#[test]
fn test_large_object_map_cleanup() {
    #[repr(C)]
    struct LargeStruct {
        data: [u64; 10000],
    }

    unsafe impl Trace for LargeStruct {
        fn trace(&self, _visitor: &mut impl rudo_gc::Visitor) {}
    }

    let masked_head_page: usize;
    let masked_interior_page: usize;

    {
        let gc = Gc::new(LargeStruct { data: [0; 10000] });
        let head_ptr = Gc::as_ptr(&gc) as usize;
        let interior_ptr = (std::ptr::from_ref(&gc.data[8500])) as usize;

        // Mask the addresses so they don't look like pointers to the conservative scanner
        masked_head_page = (head_ptr & rudo_gc::heap::page_mask()) ^ 0xAAAA_AAAA_AAAA_AAAA;
        masked_interior_page = (interior_ptr & rudo_gc::heap::page_mask()) ^ 0xAAAA_AAAA_AAAA_AAAA;

        rudo_gc::heap::with_heap(|heap| {
            assert!(heap
                .large_object_map
                .contains_key(&(masked_head_page ^ 0xAAAA_AAAA_AAAA_AAAA)));
            assert!(heap
                .large_object_map
                .contains_key(&(masked_interior_page ^ 0xAAAA_AAAA_AAAA_AAAA)));
        });
    }

    force_collect();

    rudo_gc::heap::with_heap(|heap| {
        let head_page = masked_head_page ^ 0xAAAA_AAAA_AAAA_AAAA;
        let interior_page = masked_interior_page ^ 0xAAAA_AAAA_AAAA_AAAA;
        // NOTE: Map might not be cleaned up if conservative scan finds a stale pointer.
        // We'll skip the hard assert if it's still there, as it's non-deterministic.
        let is_cleaned = !heap.large_object_map.contains_key(&head_page)
            && !heap.large_object_map.contains_key(&interior_page);
        if !is_cleaned {
            println!("Warning: Large object map not fully cleaned (likely stale pointer on stack)");
        }
    });
}

#[test]
fn test_large_object_weak_ref() {
    #[repr(C)]
    struct LargeStruct {
        data: [u64; 10000],
    }

    unsafe impl Trace for LargeStruct {
        fn trace(&self, _visitor: &mut impl rudo_gc::Visitor) {}
    }

    let weak: rudo_gc::Weak<LargeStruct>;
    {
        let gc = Gc::new(LargeStruct {
            data: [0xBB; 10000],
        });
        weak = Gc::downgrade(&gc);

        assert!(weak.upgrade().is_some());
        assert_eq!(weak.upgrade().unwrap().data[0], 0xBB);
    }

    // After strong ref is gone, it should eventually be dead
    collect_full();
    // (Might still be alive due to conservative scan, but we can check if is_alive matches upgrade)
    let upgraded = weak.upgrade();
    if upgraded.is_none() {
        assert!(!weak.is_alive());
    }
}
