// TODO https://github.com/PyO3/pyo3/issues/5487
#![allow(clippy::undocumented_unsafe_blocks)]
#![cfg(feature = "macros")]

use pyo3::class::PyTraverseError;
use pyo3::class::PyVisit;
use pyo3::ffi;
use pyo3::prelude::*;
#[cfg(not(Py_GIL_DISABLED))]
use pyo3::py_run;
#[cfg(not(target_arch = "wasm32"))]
use std::cell::Cell;
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
use std::sync::Once;
use std::sync::{Arc, Mutex};

mod test_utils;

#[pyclass(freelist = 2)]
struct ClassWithFreelist {}

#[test]
fn class_with_freelist() {
    let ptr = Python::attach(|py| {
        let inst = Py::new(py, ClassWithFreelist {}).unwrap();
        let _inst2 = Py::new(py, ClassWithFreelist {}).unwrap();
        let ptr = inst.as_ptr();
        drop(inst);
        ptr
    });

    Python::attach(|py| {
        let inst3 = Py::new(py, ClassWithFreelist {}).unwrap();
        assert_eq!(ptr, inst3.as_ptr());

        let inst4 = Py::new(py, ClassWithFreelist {}).unwrap();
        assert_ne!(ptr, inst4.as_ptr())
    });
}

#[pyclass(freelist = 2)]
#[cfg(not(target_arch = "wasm32"))]
struct ClassWithFreelistAndData {
    data: Option<usize>,
}

#[cfg(not(target_arch = "wasm32"))]
fn spin_freelist(py: Python<'_>, data: usize) {
    for _ in 0..500 {
        let inst1 = Py::new(py, ClassWithFreelistAndData { data: Some(data) }).unwrap();
        let inst2 = Py::new(py, ClassWithFreelistAndData { data: Some(data) }).unwrap();
        assert_eq!(inst1.borrow(py).data, Some(data));
        assert_eq!(inst2.borrow(py).data, Some(data));
    }
}

#[test]
#[cfg(not(target_arch = "wasm32"))]
fn multithreaded_class_with_freelist() {
    std::thread::scope(|s| {
        s.spawn(|| {
            Python::attach(|py| spin_freelist(py, 12));
        });
        s.spawn(|| {
            Python::attach(|py| spin_freelist(py, 0x4d3d3d3));
        });
    });
}

/// Helper function to create a pair of objects that can be used to test drops;
/// the first object is a guard that records when it has been dropped, the second
/// object is a check that can be used to assert that the guard has been dropped.
fn drop_check() -> (DropGuard, DropCheck) {
    let flag = Arc::new(Once::new());
    (DropGuard(flag.clone()), DropCheck(flag))
}

/// Helper structure that records when it has been dropped in the cor
struct DropGuard(Arc<Once>);
impl Drop for DropGuard {
    fn drop(&mut self) {
        self.0.call_once(|| ());
    }
}

struct DropCheck(Arc<Once>);
impl DropCheck {
    #[track_caller]
    fn assert_not_dropped(&self) {
        assert!(!self.0.is_completed());
    }

    #[track_caller]
    fn assert_dropped(&self) {
        assert!(self.0.is_completed());
    }

    #[track_caller]
    fn assert_drops_with_gc(&self, object: *mut pyo3::ffi::PyObject) {
        // running the GC might take a few cycles to collect an object
        for _ in 0..100 {
            if self.0.is_completed() {
                return;
            }

            Python::attach(|py| {
                py.run(c"import gc; gc.collect()", None, None).unwrap();
            });
            #[cfg(Py_GIL_DISABLED)]
            {
                // on the free-threaded build, the GC might be running in a separate thread, allow
                // some time for this
                std::thread::sleep(std::time::Duration::from_millis(5));
            }
        }

        panic!(
            "Object was not dropped after 100 GC cycles, refcount is {}",
            // this could be garbage, but it's in a test and we're just printing the value
            unsafe { ffi::Py_REFCNT(object) }
        );
    }
}

#[test]
fn data_is_dropped() {
    #[pyclass]
    struct DataIsDropped {
        _guard1: DropGuard,
        _guard2: DropGuard,
    }

    let (guard1, check1) = drop_check();
    let (guard2, check2) = drop_check();

    Python::attach(|py| {
        let data_is_dropped = DataIsDropped {
            _guard1: guard1,
            _guard2: guard2,
        };
        let inst = Py::new(py, data_is_dropped).unwrap();
        check1.assert_not_dropped();
        check2.assert_not_dropped();
        drop(inst);
    });

    check1.assert_dropped();
    check2.assert_dropped();
}

#[pyclass(subclass)]
struct CycleWithClear {
    cycle: Option<Py<PyAny>>,
    _guard: DropGuard,
}

#[pymethods]
impl CycleWithClear {
    fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
        visit.call(&self.cycle)
    }

    fn __clear__(slf: &Bound<'_, Self>) {
        slf.borrow_mut().cycle = None;
    }
}

#[test]
fn test_cycle_clear() {
    let (guard, check) = drop_check();

    let ptr = Python::attach(|py| {
        let inst = Bound::new(
            py,
            CycleWithClear {
                cycle: None,
                _guard: guard,
            },
        )
        .unwrap();

        inst.borrow_mut().cycle = Some(inst.clone().into_any().unbind());

        // gc.get_objects can create references to partially initialized objects,
        // leading to races on the free-threaded build.
        // see https://github.com/python/cpython/issues/130421#issuecomment-2682924142
        #[cfg(not(Py_GIL_DISABLED))]
        py_run!(py, inst, "import gc; assert inst in gc.get_objects()");
        check.assert_not_dropped();
        inst.as_ptr()
    });

    check.assert_drops_with_gc(ptr);
}

/// Test that traversing `None` of `Option<Py<T>>` does not cause a segfault
#[test]
fn gc_null_traversal() {
    #[pyclass]
    struct GcNullTraversal {
        cycle: Option<Py<Self>>,
        null: Option<Py<Self>>,
    }

    #[pymethods]
    impl GcNullTraversal {
        fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            visit.call(&self.cycle)?;
            visit.call(&self.null)?; // Should not segfault
            Ok(())
        }

        fn __clear__(&mut self) {
            self.cycle = None;
            self.null = None;
        }
    }

    Python::attach(|py| {
        let obj = Py::new(
            py,
            GcNullTraversal {
                cycle: None,
                null: None,
            },
        )
        .unwrap();
        obj.borrow_mut(py).cycle = Some(obj.clone_ref(py));

        // the object doesn't have to be cleaned up, it just needs to be traversed.
        py.run(c"import gc; gc.collect()", None, None).unwrap();
    });
}

#[test]
fn inheritance_with_new_methods_with_drop() {
    #[pyclass(subclass)]
    struct BaseClassWithDrop {
        guard: Option<DropGuard>,
    }

    #[pymethods]
    impl BaseClassWithDrop {
        #[new]
        fn new() -> BaseClassWithDrop {
            BaseClassWithDrop { guard: None }
        }
    }

    #[pyclass(extends = BaseClassWithDrop)]
    struct SubClassWithDrop {
        guard: Option<DropGuard>,
    }

    #[pymethods]
    impl SubClassWithDrop {
        #[new]
        fn new() -> PyClassInitializer<Self> {
            PyClassInitializer::from(BaseClassWithDrop { guard: None })
                .add_subclass(SubClassWithDrop { guard: None })
        }
    }

    let (guard_base, check_base) = drop_check();
    let (guard_sub, check_sub) = drop_check();

    Python::attach(|py| {
        let typeobj = py.get_type::<SubClassWithDrop>();
        let inst = typeobj
            .call((), None)
            .unwrap()
            .cast_into::<SubClassWithDrop>()
            .unwrap();

        inst.as_super().borrow_mut().guard = Some(guard_base);
        inst.borrow_mut().guard = Some(guard_sub);

        check_base.assert_not_dropped();
        check_sub.assert_not_dropped();
    });

    check_base.assert_dropped();
    check_sub.assert_dropped();
}

#[test]
fn gc_during_borrow() {
    #[pyclass]
    struct TraversableClass {
        traversed: AtomicBool,
    }

    impl TraversableClass {
        fn new() -> Self {
            Self {
                traversed: AtomicBool::new(false),
            }
        }
    }

    #[pymethods]
    impl TraversableClass {
        fn __clear__(&mut self) {}

        #[expect(clippy::unnecessary_wraps)]
        fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            self.traversed.store(true, Ordering::Relaxed);
            Ok(())
        }
    }

    Python::attach(|py| {
        // get the traverse function
        let ty = py.get_type::<TraversableClass>();
        let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };

        // create an object and check that traversing it works normally
        // when it's not borrowed
        let cell = Bound::new(py, TraversableClass::new()).unwrap();
        assert!(!cell.borrow().traversed.load(Ordering::Relaxed));
        unsafe { traverse(cell.as_ptr(), novisit, std::ptr::null_mut()) };
        assert!(cell.borrow().traversed.load(Ordering::Relaxed));

        // create an object and check that it is not traversed if the GC
        // is invoked while it is already borrowed mutably
        let cell2 = Bound::new(py, TraversableClass::new()).unwrap();
        let guard = cell2.borrow_mut();
        assert!(!guard.traversed.load(Ordering::Relaxed));
        unsafe { traverse(cell2.as_ptr(), novisit, std::ptr::null_mut()) };
        assert!(!guard.traversed.load(Ordering::Relaxed));
        drop(guard);
    });
}

#[test]
fn traverse_partial() {
    #[pyclass]
    struct PartialTraverse {
        member: Py<PyAny>,
    }

    impl PartialTraverse {
        fn new(py: Python<'_>) -> Self {
            Self { member: py.None() }
        }
    }

    #[pymethods]
    impl PartialTraverse {
        fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            visit.call(&self.member)?;
            // In the test, we expect this to never be hit
            unreachable!()
        }
    }

    Python::attach(|py| {
        // get the traverse function
        let ty = py.get_type::<PartialTraverse>();
        let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };

        // confirm that traversing errors
        let obj = Py::new(py, PartialTraverse::new(py)).unwrap();
        assert_eq!(
            unsafe { traverse(obj.as_ptr(), visit_error, std::ptr::null_mut()) },
            -1
        );
    })
}

#[test]
#[cfg(panic = "unwind")]
fn traverse_panic() {
    #[pyclass]
    struct PanickyTraverse {
        member: Py<PyAny>,
    }

    impl PanickyTraverse {
        fn new(py: Python<'_>) -> Self {
            Self { member: py.None() }
        }
    }

    #[pymethods]
    impl PanickyTraverse {
        fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            visit.call(&self.member)?;
            panic!("at the disco");
        }
    }

    Python::attach(|py| {
        // get the traverse function
        let ty = py.get_type::<PanickyTraverse>();
        let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };

        // confirm that traversing errors
        let obj = Py::new(py, PanickyTraverse::new(py)).unwrap();
        assert_eq!(
            unsafe { traverse(obj.as_ptr(), novisit, std::ptr::null_mut()) },
            -1
        );
    })
}

#[test]
#[cfg(panic = "unwind")]
fn tries_gil_in_traverse() {
    #[pyclass]
    struct TriesGILInTraverse {}

    #[pymethods]
    impl TriesGILInTraverse {
        fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            Python::attach(|_py| Ok(()))
        }
    }

    Python::attach(|py| {
        // get the traverse function
        let ty = py.get_type::<TriesGILInTraverse>();
        let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };

        // confirm that traversing panicks
        let obj = Py::new(py, TriesGILInTraverse {}).unwrap();
        assert_eq!(
            unsafe { traverse(obj.as_ptr(), novisit, std::ptr::null_mut()) },
            -1
        );
    })
}

#[test]
fn traverse_cannot_be_hijacked() {
    #[pyclass]
    struct HijackedTraverse {
        traversed: AtomicBool,
        hijacked: AtomicBool,
    }

    impl HijackedTraverse {
        fn new() -> Self {
            Self {
                traversed: AtomicBool::new(false),
                hijacked: AtomicBool::new(false),
            }
        }

        fn traversed_and_hijacked(&self) -> (bool, bool) {
            (
                self.traversed.load(Ordering::Acquire),
                self.hijacked.load(Ordering::Acquire),
            )
        }
    }

    #[pymethods]
    impl HijackedTraverse {
        #[expect(clippy::unnecessary_wraps)]
        fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            self.traversed.store(true, Ordering::Release);
            Ok(())
        }
    }

    #[allow(dead_code, reason = "used to test the trait method is not used")]
    trait Traversable {
        fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>;
    }

    impl Traversable for PyRef<'_, HijackedTraverse> {
        fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            self.hijacked.store(true, Ordering::Release);
            Ok(())
        }
    }

    Python::attach(|py| {
        // get the traverse function
        let ty = py.get_type::<HijackedTraverse>();
        let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };

        let cell = Bound::new(py, HijackedTraverse::new()).unwrap();
        assert_eq!(cell.borrow().traversed_and_hijacked(), (false, false));
        unsafe { traverse(cell.as_ptr(), novisit, std::ptr::null_mut()) };
        assert_eq!(cell.borrow().traversed_and_hijacked(), (true, false));
    })
}

#[pyclass]
struct DropDuringTraversal {
    cycle: Mutex<Option<Py<Self>>>,
    _guard: DropGuard,
}

#[pymethods]
impl DropDuringTraversal {
    #[expect(clippy::unnecessary_wraps)]
    fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
        let mut cycle_ref = self.cycle.lock().unwrap();
        *cycle_ref = None;
        Ok(())
    }
}

#[cfg(not(pyo3_disable_reference_pool))]
#[test]
fn drop_during_traversal_with_gil() {
    let (guard, check) = drop_check();

    let ptr = Python::attach(|py| {
        let cycle = Mutex::new(None);
        let inst = Py::new(
            py,
            DropDuringTraversal {
                cycle,
                _guard: guard,
            },
        )
        .unwrap();

        *inst.borrow_mut(py).cycle.lock().unwrap() = Some(inst.clone_ref(py));

        check.assert_not_dropped();
        let ptr = inst.as_ptr();
        drop(inst); // drop the object while holding the GIL

        #[cfg(not(Py_GIL_DISABLED))]
        {
            // other thread might have caused GC on free-threaded build
            check.assert_not_dropped();
        }

        ptr
    });

    check.assert_drops_with_gc(ptr);
}

#[cfg(not(pyo3_disable_reference_pool))]
#[test]
fn drop_during_traversal_without_gil() {
    let (guard, check) = drop_check();

    let inst = Python::attach(|py| {
        let cycle = Mutex::new(None);
        let inst = Py::new(
            py,
            DropDuringTraversal {
                cycle,
                _guard: guard,
            },
        )
        .unwrap();

        *inst.borrow_mut(py).cycle.lock().unwrap() = Some(inst.clone_ref(py));

        check.assert_not_dropped();
        inst
    });

    let ptr = inst.as_ptr();
    drop(inst); // drop the object without holding the GIL

    check.assert_drops_with_gc(ptr);
}

#[test]
#[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled
fn unsendable_are_not_traversed_on_foreign_thread() {
    #[pyclass(unsendable)]
    struct UnsendableTraversal {
        traversed: Cell<bool>,
    }

    #[pymethods]
    impl UnsendableTraversal {
        fn __clear__(&mut self) {}

        #[expect(clippy::unnecessary_wraps)]
        fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            self.traversed.set(true);
            Ok(())
        }
    }

    #[derive(Clone, Copy)]
    struct SendablePtr(*mut pyo3::ffi::PyObject);

    unsafe impl Send for SendablePtr {}

    Python::attach(|py| {
        let ty = py.get_type::<UnsendableTraversal>();
        let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };

        let obj = Bound::new(
            py,
            UnsendableTraversal {
                traversed: Cell::new(false),
            },
        )
        .unwrap();

        let ptr = SendablePtr(obj.as_ptr());

        std::thread::spawn(move || {
            // traversal on foreign thread is a no-op
            assert_eq!(
                unsafe { traverse({ ptr }.0, novisit, std::ptr::null_mut()) },
                0
            );
        })
        .join()
        .unwrap();

        assert!(!obj.borrow().traversed.get());

        // traversal on home thread still works
        assert_eq!(
            unsafe { traverse({ ptr }.0, novisit, std::ptr::null_mut()) },
            0
        );

        assert!(obj.borrow().traversed.get());
    });
}

#[test]
fn test_traverse_subclass() {
    #[pyclass(extends = CycleWithClear)]
    struct SubOverrideTraverse {}

    #[pymethods]
    impl SubOverrideTraverse {
        #[expect(clippy::unnecessary_wraps)]
        fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            // subclass traverse overrides the base class traverse
            Ok(())
        }
    }

    let (guard, check) = drop_check();

    Python::attach(|py| {
        let base = CycleWithClear {
            cycle: None,
            _guard: guard,
        };
        let obj = Bound::new(
            py,
            PyClassInitializer::from(base).add_subclass(SubOverrideTraverse {}),
        )
        .unwrap();
        obj.borrow_mut().as_super().cycle = Some(obj.clone().into_any().unbind());

        check.assert_not_dropped();
        let ptr = obj.as_ptr();
        drop(obj);
        #[cfg(not(Py_GIL_DISABLED))]
        {
            // other thread might have caused GC on free-threaded build
            check.assert_not_dropped();
        }

        check.assert_drops_with_gc(ptr);
    });
}

#[test]
fn test_traverse_subclass_override_clear() {
    #[pyclass(extends = CycleWithClear)]
    struct SubOverrideClear {}

    #[pymethods]
    impl SubOverrideClear {
        #[expect(clippy::unnecessary_wraps)]
        fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            // subclass traverse overrides the base class traverse, necessary for
            // the sub clear to be called
            // FIXME: should this really need to be the case?
            Ok(())
        }

        fn __clear__(&self) {
            // subclass clear overrides the base class clear
        }
    }

    let (guard, check) = drop_check();

    Python::attach(|py| {
        let base = CycleWithClear {
            cycle: None,
            _guard: guard,
        };
        let obj = Bound::new(
            py,
            PyClassInitializer::from(base).add_subclass(SubOverrideClear {}),
        )
        .unwrap();
        obj.borrow_mut().as_super().cycle = Some(obj.clone().into_any().unbind());

        check.assert_not_dropped();
        let ptr = obj.as_ptr();
        drop(obj);
        #[cfg(not(Py_GIL_DISABLED))]
        {
            // other thread might have caused GC on free-threaded build
            check.assert_not_dropped();
        }

        check.assert_drops_with_gc(ptr);
    });
}

// Manual traversal utilities

unsafe fn get_type_traverse(tp: *mut pyo3::ffi::PyTypeObject) -> Option<pyo3::ffi::traverseproc> {
    unsafe { std::mem::transmute(pyo3::ffi::PyType_GetSlot(tp, pyo3::ffi::Py_tp_traverse)) }
}

// a dummy visitor function
extern "C" fn novisit(
    _object: *mut pyo3::ffi::PyObject,
    _arg: *mut core::ffi::c_void,
) -> std::ffi::c_int {
    0
}

// a visitor function which errors (returns nonzero code)
extern "C" fn visit_error(
    _object: *mut pyo3::ffi::PyObject,
    _arg: *mut core::ffi::c_void,
) -> std::ffi::c_int {
    -1
}

// the fields visited below, set before driving the traversal
static BASE_FIELD: AtomicPtr<pyo3::ffi::PyObject> = AtomicPtr::new(std::ptr::null_mut());
static CHILD_FIELD: AtomicPtr<pyo3::ffi::PyObject> = AtomicPtr::new(std::ptr::null_mut());
static BASE_VISITED: AtomicBool = AtomicBool::new(false);
static CHILD_VISITED: AtomicBool = AtomicBool::new(false);

// a visitor function which errors on `BASE_FIELD` only
extern "C" fn visit_error_on_base_field(
    object: *mut pyo3::ffi::PyObject,
    _arg: *mut core::ffi::c_void,
) -> std::ffi::c_int {
    if object == CHILD_FIELD.load(Ordering::SeqCst) {
        CHILD_VISITED.store(true, Ordering::SeqCst);
    }
    if object == BASE_FIELD.load(Ordering::SeqCst) {
        BASE_VISITED.store(true, Ordering::SeqCst);
        return -1;
    }
    0
}

#[test]
#[cfg(any(not(Py_LIMITED_API), Py_3_11))] // buffer availability
fn test_drop_buffer_during_traversal_without_gil() {
    use pyo3::buffer::PyBuffer;
    use pyo3::types::PyBytes;

    // `PyBuffer` has a drop method which attempts to attach to the Python interpreter,
    // if the thread is during traverse we leak it for safety. This should _never_ be happening
    // so it's purely a user bug, but we leak to be safe.

    #[pyclass]
    struct BufferDropDuringTraversal {
        inner: Mutex<Option<(DropGuard, PyBuffer<u8>)>>,
        cycle: Option<Py<PyAny>>,
    }

    #[pymethods]
    impl BufferDropDuringTraversal {
        #[expect(clippy::unnecessary_wraps)]
        fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            self.inner.lock().unwrap().take();
            Ok(())
        }

        fn __clear__(&mut self) {
            self.cycle = None;
        }
    }

    let (guard, check) = drop_check();
    Python::attach(|py| {
        let obj = Py::new(
            py,
            BufferDropDuringTraversal {
                inner: Mutex::new(Some((
                    guard,
                    PyBuffer::get(&PyBytes::new(py, b"test")).unwrap(),
                ))),
                cycle: None,
            },
        )
        .unwrap();

        obj.borrow_mut(py).cycle = Some(obj.clone_ref(py).into_any());

        let ptr = obj.as_ptr();
        drop(obj);

        check.assert_drops_with_gc(ptr);
    });
}

#[test]
fn test_super_traverse_early_return_does_not_abort() {
    #[pyclass(subclass)]
    struct TraverseBase {
        field: Py<PyAny>,
    }

    #[pymethods]
    impl TraverseBase {
        fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            visit.call(&self.field)
        }
    }

    #[pyclass(extends=TraverseBase)]
    struct TraverseChild {
        field: Py<PyAny>,
    }

    #[pymethods]
    impl TraverseChild {
        fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
            visit.call(&self.field)
        }
    }

    Python::attach(|py| {
        let base_field = pyo3::types::PyList::empty(py).into_any().unbind();
        let child_field = pyo3::types::PyList::empty(py).into_any().unbind();

        let initializer = PyClassInitializer::from(TraverseBase {
            field: base_field.clone_ref(py),
        })
        .add_subclass(TraverseChild {
            field: child_field.clone_ref(py),
        });
        let child = Bound::new(py, initializer).unwrap();

        BASE_FIELD.store(base_field.as_ptr(), Ordering::SeqCst);
        CHILD_FIELD.store(child_field.as_ptr(), Ordering::SeqCst);

        let traverse =
            unsafe { get_type_traverse(py.get_type::<TraverseChild>().as_type_ptr()).unwrap() };

        let retval = unsafe {
            traverse(
                child.as_ptr(),
                visit_error_on_base_field,
                std::ptr::null_mut(),
            )
        };

        assert!(
            BASE_VISITED.load(Ordering::SeqCst),
            "super-type traverse never visited its field, so the early return was not exercised"
        );
        assert_eq!(
            retval, -1,
            "traverse did not propagate the non-zero return of the super-type traverse"
        );
        assert!(
            !CHILD_VISITED.load(Ordering::SeqCst),
            "child __traverse__ ran despite the super-type traverse returning non-zero"
        );
    });
}

// A `#[pyclass(dict)]` can form a reference cycle through its instance `__dict__`
// (`obj.attr = obj`). The tests below cover each valid combination of user-defined
// `__traverse__` / `__clear__`; a `__clear__` without a `__traverse__` is rejected at
// type-creation time.

#[test]
fn dict_class_is_a_gc_type() {
    Python::attach(|py| {
        let ty = py.get_type::<DictCycleNoTraverse>();
        let flags = unsafe { ffi::PyType_GetFlags(ty.as_type_ptr()) };
        assert_ne!(flags & ffi::Py_TPFLAGS_HAVE_GC, 0);
    });
}

/// `#[pyclass(dict)]` with neither `__traverse__` nor `__clear__`: both slots are synthesized.
#[pyclass(dict)]
struct DictCycleNoTraverse {
    _guard: DropGuard,
}

#[test]
fn dict_cycle_collected_without_traverse() {
    let (guard, check) = drop_check();

    let ptr = Python::attach(|py| {
        let inst = Bound::new(py, DictCycleNoTraverse { _guard: guard }).unwrap();
        // Reference cycle through the instance `__dict__`: inst.__dict__["cycle"] -> inst.
        inst.setattr("cycle", &inst).unwrap();
        check.assert_not_dropped();
        inst.as_ptr()
    });

    check.assert_drops_with_gc(ptr);
}

/// `#[pyclass(dict)]` with `__traverse__` but no `__clear__`: the `__dict__` is visited by
/// `_call_traverse` and cleared by a synthesized `tp_clear`.
#[pyclass(dict)]
struct DictCycleTraverseOnly {
    _guard: DropGuard,
}

#[pymethods]
impl DictCycleTraverseOnly {
    #[expect(clippy::unnecessary_wraps)]
    fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
        // No Rust references to visit; the `__dict__` is visited by `_call_traverse` itself.
        Ok(())
    }
}

#[test]
fn dict_cycle_collected_with_traverse_only() {
    let (guard, check) = drop_check();

    let ptr = Python::attach(|py| {
        let inst = Bound::new(py, DictCycleTraverseOnly { _guard: guard }).unwrap();
        inst.setattr("cycle", &inst).unwrap();
        check.assert_not_dropped();
        inst.as_ptr()
    });

    check.assert_drops_with_gc(ptr);
}

/// `#[pyclass(dict)]` with both `__traverse__` and `__clear__`: the `__dict__` is folded into
/// the user-defined slots by `_call_traverse` / `_call_clear`.
#[pyclass(dict)]
struct DictCycleTraverseAndClear {
    _guard: DropGuard,
    field: Option<Py<PyAny>>,
}

#[pymethods]
impl DictCycleTraverseAndClear {
    fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
        if let Some(field) = &self.field {
            visit.call(field)?;
        }
        Ok(())
    }

    fn __clear__(&mut self) {
        self.field = None;
    }
}

#[test]
fn dict_cycle_collected_with_traverse_and_clear() {
    let (guard, check) = drop_check();

    let ptr = Python::attach(|py| {
        let inst = Bound::new(
            py,
            DictCycleTraverseAndClear {
                _guard: guard,
                field: None,
            },
        )
        .unwrap();
        inst.setattr("cycle", &inst).unwrap();
        check.assert_not_dropped();
        inst.as_ptr()
    });

    check.assert_drops_with_gc(ptr);
}
