// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 John Ray <996351336@qq.com>

//! Integration tests for gc-lite garbage collection system
//!
//! Tests cover:
//! - Partition management
//! - Object allocation and memory tracking
//! - Garbage collection
//! - Root object management
//! - Weak references
//! - Error handling

use gc_lite::{GcError, GcHeap, GcPartitionId, GcRef, GcTrace, GcTraceCtx, gc_type_register};

/// Test data structure for integration tests
#[derive(Debug, PartialEq, Clone)]
struct TestData {
    value: i32,
    name: String,
}

impl GcTrace for TestData {
    fn trace(&self, _: &mut GcTraceCtx) {}
}

/// Test node structure with GC references
struct TestNode {
    value: i32,
    children: Vec<GcRef<TestNode>>,
}

impl GcTrace for TestNode {
    fn trace(&self, tr: &mut GcTraceCtx) {
        for child in &self.children {
            tr.add(*child);
        }
    }
}

impl core::fmt::Debug for TestNode {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("TestNode")
            .field("value", &self.value)
            .field("children_count", &self.children.len())
            .finish()
    }
}

impl TestNode {
    fn new(value: i32) -> Self {
        Self {
            value,
            children: Vec::new(),
        }
    }

    fn add_child(&mut self, child: GcRef<TestNode>) {
        self.children.push(child);
    }
}

gc_type_register! {
    TestData, drop_pass = 0;
    TestNode, drop_pass = 0;
}

// ============ Partition Management Tests ============

#[test]
fn test_partition_creation_and_retrieval() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);

    // Create partitions
    let id1 = heap.create_partition();
    let id2 = heap.create_partition();

    assert_ne!(id1, id2);
    assert_eq!(heap.partition_ids().len(), 2);

    // Verify partition info
    let partition = heap.partition(id1).unwrap();
    assert_eq!(partition.memory_used(), 0);
}

#[test]
fn test_partition_removal() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    assert!(heap.partition(id).is_some());
    assert_eq!(heap.partition_ids().len(), 1);

    heap.remove_partition(id, GcHeap::DUMMY_DISPOSE_CALLBACK);

    assert!(heap.partition(id).is_none());
    assert_eq!(heap.partition_ids().len(), 0);
}

#[test]
fn test_partition_gc_threshold() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    heap.set_memory_limit(1024);

    // Default threshold should be 0 (disabled)
    assert_eq!(heap.gc_threshold(), 0);

    // Set threshold
    heap.set_gc_threshold(512);
    assert_eq!(heap.gc_threshold(), 512);

    // Set threshold exceeding limit should auto-adjust to 0.8x of limit
    heap.set_gc_threshold(2048);
    // 1024 * 8 / 10 = 819
    assert_eq!(heap.gc_threshold(), 819);
}

// ============ Memory Limit Tests ============

#[test]
fn test_allocation_fails_when_limit_exceeded() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    heap.set_memory_limit(256); // Small global limit
    let id = heap.create_partition();

    // Allocate objects until we hit the limit
    let mut allocated_count = 0;
    loop {
        match unsafe {
            heap.alloc_raw(
                id,
                TestData {
                    value: allocated_count,
                    name: format!("obj_{}", allocated_count),
                },
            )
        } {
            Ok(_) => {
                allocated_count += 1;
            }
            Err((GcError::PartitionFull, _)) => {
                // Expected when partition is full
                break;
            }
            Err((err, _)) => {
                panic!("Unexpected error: {:?}", err);
            }
        }
    }

    // Verify some objects were allocated
    assert!(allocated_count > 0);

    // Try to allocate one more - should fail
    let result = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 999,
                name: "should_fail".to_string(),
            },
        )
    };

    assert!(matches!(result, Err((GcError::PartitionFull, _))));
}

#[test]
fn test_set_memory_limit_above_used_memory() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Allocate some objects to use memory
    let _obj1 = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 1,
                name: "obj1".to_string(),
            },
        )
    }
    .unwrap();

    let _obj2 = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 2,
                name: "obj2".to_string(),
            },
        )
    }
    .unwrap();

    let used_memory = heap.partition(id).unwrap().memory_used();
    assert!(used_memory > 0);

    // Set limit larger than used memory - should work
    let new_limit = used_memory + 512;
    let applied = heap.set_memory_limit(new_limit);
    assert_eq!(applied, new_limit);

    // Verify limit was set correctly
    assert_eq!(heap.memory_limit(), new_limit);

    // Should be able to allocate more objects
    let _obj3 = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 3,
                name: "obj3".to_string(),
            },
        )
    }
    .unwrap();
}

#[test]
fn test_set_memory_limit_below_used_memory() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Allocate some objects to use memory
    let _obj1 = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 1,
                name: "obj1".to_string(),
            },
        )
    }
    .unwrap();

    let _obj2 = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 2,
                name: "obj2".to_string(),
            },
        )
    }
    .unwrap();

    let used_memory = heap.partition(id).unwrap().memory_used();
    assert!(used_memory > 0);

    // Set limit smaller than used memory - should be adjusted to used memory
    let smaller_limit = used_memory - 1;
    let applied_limit = heap.set_memory_limit(smaller_limit);

    // The limit should be adjusted to at least the used memory
    assert_eq!(applied_limit, used_memory);
    assert_eq!(heap.memory_limit(), used_memory);

    // Should still be able to allocate (because limit >= used_memory)
    // But not more than the limit allows
    // Note: Since limit == used_memory, no new allocations should be allowed
    let result = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 3,
                name: "should_fail".to_string(),
            },
        )
    };

    assert!(matches!(result, Err((GcError::PartitionFull, _))));
}

#[test]
fn test_set_unlimited_memory() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Set limit to 0 (unlimited)
    heap.set_memory_limit(0);

    // Verify limit is 0 (unlimited)
    assert_eq!(heap.memory_limit(), 0);

    // Should be able to allocate many objects without hitting limit
    let mut allocated_count = 0;
    loop {
        match unsafe {
            heap.alloc_raw(
                id,
                TestData {
                    value: allocated_count,
                    name: format!("obj_{}", allocated_count),
                },
            )
        } {
            Ok(_) => {
                allocated_count += 1;
                // Stop after a reasonable number to avoid infinite loop
                if allocated_count >= 100 {
                    break;
                }
            }
            Err((err, _)) => {
                panic!("Unexpected error with unlimited memory: {:?}", err);
            }
        }
    }

    assert_eq!(allocated_count, 100);
}

// ============ Object Allocation Tests ============

#[test]
fn test_object_allocation() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Get initial memory usage
    let initial_memory = heap.partition(id).unwrap().memory_used();

    // Allocate an object
    let obj: GcRef<TestData> = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 42,
                name: "test".to_string(),
            },
        )
    }
    .unwrap();

    // Verify partition memory was updated
    let partition = heap.partition(id).unwrap();
    let after_memory = partition.memory_used();
    assert!(after_memory > initial_memory);

    // Verify object content
    assert_eq!(obj.value, 42);
    assert_eq!(obj.name, "test");
}

#[test]
fn test_memory_usage_increases_with_allocation() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Track memory usage after each allocation
    let mut memory_after_each_alloc: Vec<usize> = Vec::new();

    // Allocate multiple objects and track memory
    for i in 0..5 {
        let _obj = unsafe {
            heap.alloc_raw(
                id,
                TestData {
                    value: i,
                    name: format!("obj_{}", i),
                },
            )
        }
        .expect("allocation failed");

        let memory = heap.partition(id).unwrap().memory_used();
        memory_after_each_alloc.push(memory);
    }

    // Verify memory increases with each allocation
    for i in 1..memory_after_each_alloc.len() {
        assert!(
            memory_after_each_alloc[i] > memory_after_each_alloc[i - 1],
            "Memory should increase after each allocation"
        );
    }

    // Verify cumulative memory is correct (each object adds GcHead + T size)
    let final_memory = heap.partition(id).unwrap().memory_used();
    assert!(final_memory > 0);

    // Verify memory was freed after GC
    unsafe {
        heap.alloc_root_raw(
            id,
            TestData {
                value: 100,
                name: "root".to_string(),
            },
        )
    }
    .unwrap();
    let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
    assert!(freed > 0);

    let after_gc_memory = heap.partition(id).unwrap().memory_used();
    assert!(after_gc_memory < final_memory);
}

#[test]
fn test_multiple_object_allocation() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Allocate multiple objects
    let mut objs: Vec<GcRef<TestData>> = Vec::new();
    for i in 0..10 {
        let obj = unsafe {
            heap.alloc_raw(
                id,
                TestData {
                    value: i,
                    name: format!("obj_{}", i),
                },
            )
        }
        .expect("allocation failed");
        objs.push(obj);
    }

    // Verify all objects
    for (i, obj) in objs.iter().enumerate() {
        assert_eq!(obj.value, i as i32);
        assert_eq!(obj.name, format!("obj_{}", i));
    }

    // Verify memory tracking
    let partition = heap.partition(id).unwrap();
    assert!(partition.memory_used() > 0);
}

#[test]
fn test_partition_full_error() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    // Use a small global heap limit so allocations will eventually fail
    heap.set_memory_limit(512);
    let id = heap.create_partition();

    // Try to allocate objects until partition is full
    let mut result = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 0,
                name: "test".to_string(),
            },
        )
    };

    // Keep trying until we get a PartitionFull error
    let mut count = 0;
    while let Ok(_obj) = result {
        count += 1;
        result = unsafe {
            heap.alloc_raw(
                id,
                TestData {
                    value: count,
                    name: format!("test_{}", count),
                },
            )
        };
    }

    assert!(matches!(result, Err((GcError::PartitionFull, _))));
    assert!(count > 0);
}

#[test]
fn test_invalid_partition_allocation() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let invalid_id = GcPartitionId(9999);

    let result = unsafe {
        heap.alloc_raw(
            invalid_id,
            TestData {
                value: 42,
                name: "test".to_string(),
            },
        )
    };

    assert!(matches!(result, Err((GcError::PartitionNotFound, _))));
}

// ============ Root Object Tests ============

#[test]
fn test_root_object_management() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    let obj = unsafe {
        heap.alloc_root_raw(
            id,
            TestData {
                value: 42,
                name: "test".to_string(),
            },
        )
    }
    .unwrap();

    // Initially not a root
    assert!(obj.is_root());
}

#[test]
fn test_root_objects_preserve_during_gc() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    let obj = unsafe {
        heap.alloc_root_raw(
            id,
            TestData {
                value: 42,
                name: "test".to_string(),
            },
        )
    }
    .unwrap();

    // Trigger GC
    let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
    assert_eq!(freed, 0);

    // Object should still be valid
    assert_eq!(obj.value, 42);
}

#[test]
fn test_non_root_objects_collected() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Create two objects, one is root, one is not
    let root_obj = unsafe {
        heap.alloc_root_raw(
            id,
            TestData {
                value: 1,
                name: "root".to_string(),
            },
        )
    }
    .unwrap();

    let _non_root_obj = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 2,
                name: "non_root".to_string(),
            },
        )
    }
    .unwrap();

    // Trigger GC
    let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
    assert!(freed > 0);

    // Root object should still be valid
    assert_eq!(root_obj.value, 1);
}

// ============ Garbage Collection Tests ============

#[test]
fn test_manual_garbage_collection() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Create objects with some as roots
    for i in 0..5 {
        if i < 2 {
            let _obj = unsafe {
                heap.alloc_root_raw(
                    id,
                    TestData {
                        value: i,
                        name: format!("obj_{}", i),
                    },
                )
            }
            .unwrap();
        } else {
            let _obj = unsafe {
                heap.alloc_raw(
                    id,
                    TestData {
                        value: i,
                        name: format!("obj_{}", i),
                    },
                )
            }
            .unwrap();
        }
    }

    // Get initial memory usage
    let before = heap.partition(id).unwrap().memory_used();

    // Trigger GC
    let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);

    // Should have freed some memory
    assert!(freed > 0);

    // Verify root objects preserved
    let after = heap.partition(id).unwrap().memory_used();
    // Memory used should be less than before
    assert!(after < before);
}

#[test]
fn test_circular_reference_handling() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Allocate nodes and create a circular reference
    let mut node1 = unsafe { heap.alloc_raw(id, TestNode::new(1)) }.unwrap();
    let mut node2 = unsafe { heap.alloc_raw(id, TestNode::new(2)) }.unwrap();
    node1.with_mut(&mut heap, |n| n.add_child(node2));
    node2.with_mut(&mut heap, |n| n.add_child(node1));

    // To test collection, we need a root that references the cycle
    let mut root = unsafe { heap.alloc_root_raw(id, TestNode::new(0)) }.unwrap();
    root.with_mut(&mut heap, |n| n.add_child(node1));

    // Now, break the link from the root to the cycle
    root.with_mut(&mut heap, |n| n.children.clear());

    // GC should now collect the cycle
    let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
    assert!(freed > 0);

    // GC should not collect anything yet
    let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
    assert_eq!(freed, 0);
}

// ============ Weak Reference Tests ============

#[test]
fn test_weak_reference_creation_and_upgrade() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Create object and weak reference
    let obj = unsafe {
        heap.alloc_root_raw(
            id,
            TestData {
                value: 42,
                name: "test".to_string(),
            },
        )
    }
    .unwrap();

    let weak_ref = heap.downgrade(&obj);

    // Upgrade should succeed while object exists
    let upgraded = weak_ref.upgrade(&heap);
    assert!(upgraded.is_some());

    let upgraded_ref = upgraded.unwrap();
    assert_eq!(upgraded_ref.value, 42);
}

#[test]
fn test_weak_reference_after_collection() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    // Create object and weak reference
    let obj = unsafe {
        heap.alloc_raw(
            id,
            TestData {
                value: 42,
                name: "test".to_string(),
            },
        )
    }
    .unwrap();

    let weak_ref = heap.downgrade(&obj);

    // Collect garbage. Since obj is not a root, it should be collected.
    heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);

    // Upgrade should fail after object is collected
    let upgraded = weak_ref.upgrade(&heap);
    assert!(upgraded.is_none());
}

#[test]
fn test_multiple_weak_references() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
    let id = heap.create_partition();

    let obj = unsafe {
        heap.alloc_root_raw(
            id,
            TestData {
                value: 42,
                name: "test".to_string(),
            },
        )
    }
    .unwrap();

    // Create multiple weak references
    let weak1 = heap.downgrade(&obj);
    let weak2 = heap.downgrade(&obj);
    let weak3 = heap.downgrade(&obj);

    // All should upgrade successfully
    assert!(weak1.upgrade(&heap).is_some());
    assert!(weak2.upgrade(&heap).is_some());
    assert!(weak3.upgrade(&heap).is_some());
}

#[test]
fn test_weak_reference_after_partition_removal() {
    let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);

    // 创建两个同级的 partitions
    heap.create_partition();
    let child_id = heap.create_partition();

    // 在下属partition创建对象
    let obj = unsafe {
        heap.alloc_raw(
            child_id,
            TestData {
                value: 42,
                name: "test".to_string(),
            },
        )
    }
    .unwrap();

    // 记录这些对象的GcWeak
    let weak_ref = heap.downgrade(&obj);

    // 验证弱引用可以升级
    assert!(weak_ref.upgrade(&heap).is_some());

    // 删除partition
    heap.remove_partition(child_id, GcHeap::DUMMY_DISPOSE_CALLBACK);

    // 这时此partition中的对象也会释放
    // 访问这些对象的GcWeak引用，并upgrade()，应该返回None
    let upgraded = weak_ref.upgrade(&heap);
    assert!(upgraded.is_none());
}

// ============ Context Detection Tests ============

#[test]
fn test_contains_method() {
    let mut heap1 = GcHeap::new(&GC_TYPE_REGISTRY);
    let mut heap2 = GcHeap::new(&GC_TYPE_REGISTRY);

    let id1 = heap1.create_partition();
    let id2 = heap2.create_partition();

    let obj1 = unsafe {
        heap1.alloc_raw(
            id1,
            TestData {
                value: 1,
                name: "heap1".to_string(),
            },
        )
    }
    .unwrap();

    let obj2 = unsafe {
        heap2.alloc_raw(
            id2,
            TestData {
                value: 2,
                name: "heap2".to_string(),
            },
        )
    }
    .unwrap();

    // Verify object ownership
    assert!(heap1.contains(obj1.node_ptr()));
    assert!(!heap1.contains(obj2.node_ptr()));
    assert!(heap2.contains(obj2.node_ptr()));
    assert!(!heap2.contains(obj1.node_ptr()));
}

// ============ Reference Recovery Tests ============

// Note: try_from_ref requires exact type match including function pointers
// These tests are simplified to avoid type registration complexity
