#![cfg(feature = "macros")]

use pyo3::prelude::*;
use pyo3::py_run;
use pyo3::types::IntoPyDict;

mod test_utils;

/// Macro to generate refcount leak tests for types.
/// Ensures that creating and destroying instances doesn't leak references to the type.
/// Regression test for issues #1363 and #6223.
#[cfg(not(Py_GIL_DISABLED))]
macro_rules! assert_type_refcount_stable {
    // Simple case: type with parameterless constructor
    ($type_name:ty) => {
        assert_type_refcount_stable!($type_name, stringify!($type_name), "Type()");
    };
    // With custom constructor
    ($type_name:ty, $test_name:expr, $ctor:expr) => {{
        Python::attach(|py| {
            #[expect(non_snake_case)]
            let Type = py.get_type::<$type_name>();
            let ctor_code = $ctor;
            py_run!(
                py,
                Type,
                &format!(
                    r#"
                        import gc
                        import sys

                        gc.collect()
                        count = sys.getrefcount(Type)

                        for i in range(1000):
                            obj = {}
                            del obj

                        gc.collect()
                        after = sys.getrefcount(Type)
                        assert after == count, f"Type ref count leaked: {{after}} vs {{count}}"
                        "#,
                    ctor_code
                )
            );
        });
    }};
}

#[pyclass(subclass)]
struct BaseClass {
    #[pyo3(get)]
    val1: usize,
}

#[pyclass(subclass)]
struct SubclassAble {}

#[test]
fn subclass() {
    Python::attach(|py| {
        let d = [("SubclassAble", py.get_type::<SubclassAble>())]
            .into_py_dict(py)
            .unwrap();

        py.run(
            c"class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)",
            None,
            Some(&d),
        )
        .map_err(|e| e.display(py))
        .unwrap();
    });
}

#[pymethods]
impl BaseClass {
    #[new]
    fn new() -> Self {
        BaseClass { val1: 10 }
    }
    fn base_method(&self, x: usize) -> usize {
        x * self.val1
    }
    fn base_set(&mut self, fn_: &Bound<'_, PyAny>) -> PyResult<()> {
        let value: usize = fn_.call0()?.extract()?;
        self.val1 = value;
        Ok(())
    }
}

#[pyclass(extends=BaseClass)]
struct SubClass {
    #[pyo3(get)]
    val2: usize,
}

#[pymethods]
impl SubClass {
    #[new]
    fn new() -> PyClassInitializer<Self> {
        PyClassInitializer::from(BaseClass { val1: 10 }).add_subclass(SubClass { val2: 5 })
    }
    fn sub_method(&self, x: usize) -> usize {
        x * self.val2
    }
    fn sub_set_and_ret(&mut self, x: usize) -> usize {
        self.val2 = x;
        x
    }
}

#[test]
fn inheritance_with_new_methods() {
    Python::attach(|py| {
        let typeobj = py.get_type::<SubClass>();
        let inst = typeobj.call((), None).unwrap();
        py_run!(py, inst, "assert inst.val1 == 10; assert inst.val2 == 5");
    });
}

#[test]
fn call_base_and_sub_methods() {
    Python::attach(|py| {
        let obj = Py::new(py, SubClass::new()).unwrap();
        py_run!(
            py,
            obj,
            r#"
    assert obj.base_method(10) == 100
    assert obj.sub_method(10) == 50
"#
        );
    });
}

#[test]
fn mutation_fails() {
    Python::attach(|py| {
        let obj = Py::new(py, SubClass::new()).unwrap();
        let global = [("obj", obj)].into_py_dict(py).unwrap();
        let e = py
            .run(
                c"obj.base_set(lambda: obj.sub_set_and_ret(1))",
                Some(&global),
                None,
            )
            .unwrap_err();
        assert_eq!(&e.to_string(), "RuntimeError: Already borrowed");
    });
}

#[test]
fn is_subclass_and_is_instance() {
    Python::attach(|py| {
        let sub_ty = py.get_type::<SubClass>();
        let base_ty = py.get_type::<BaseClass>();
        assert!(sub_ty.is_subclass_of::<BaseClass>().unwrap());
        assert!(sub_ty.is_subclass(&base_ty).unwrap());

        let obj = Bound::new(py, SubClass::new()).unwrap().into_any();
        assert!(obj.is_instance_of::<SubClass>());
        assert!(obj.is_instance_of::<BaseClass>());
        assert!(obj.is_instance(&sub_ty).unwrap());
        assert!(obj.is_instance(&base_ty).unwrap());
    });
}

#[pyclass(subclass)]
struct BaseClassWithResult {
    _val: usize,
}

#[pymethods]
impl BaseClassWithResult {
    #[new]
    fn new(value: isize) -> PyResult<Self> {
        Ok(Self {
            _val: std::convert::TryFrom::try_from(value)?,
        })
    }
}

#[pyclass(extends=BaseClassWithResult)]
struct SubClass2 {}

#[pymethods]
impl SubClass2 {
    #[new]
    fn new(value: isize) -> PyResult<PyClassInitializer<Self>> {
        let base = BaseClassWithResult::new(value)?;
        Ok(PyClassInitializer::from(base).add_subclass(Self {}))
    }
}

#[test]
fn handle_result_in_new() {
    Python::attach(|py| {
        let subclass = py.get_type::<SubClass2>();
        py_run!(
            py,
            subclass,
            r#"
try:
    subclass(-10)
    assert False
except ValueError as e:
    pass
except Exception as e:
    raise e
"#
        );
    });
}

// Subclassing builtin types is not possible in the LIMITED API before 3.12
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
mod inheriting_native_type {
    use super::*;
    use pyo3::exceptions::PyException;

    #[cfg(not(GraalPy))]
    use {
        pyo3::types::{PyCapsule, PyDict},
        std::sync::{
            atomic::{AtomicBool, Ordering},
            Arc,
        },
    };

    #[cfg(not(any(PyPy, GraalPy)))]
    use pyo3::types::PySet;

    #[cfg(not(any(PyPy, GraalPy)))]
    #[pyclass(extends=PySet)]
    #[derive(Debug)]
    pub struct SetWithName {
        #[pyo3(get, name = "name")]
        _name: &'static str,
    }

    #[cfg(not(any(PyPy, GraalPy)))]
    #[pymethods]
    impl SetWithName {
        #[new]
        fn new() -> Self {
            SetWithName { _name: "Hello :)" }
        }
    }

    #[cfg(not(any(PyPy, GraalPy)))]
    #[test]
    fn inherit_set() {
        Python::attach(|py| {
            let set_sub = pyo3::Py::new(py, SetWithName::new()).unwrap();
            py_run!(
                py,
                set_sub,
                r#"set_sub.add(10); assert list(set_sub) == [10]; assert set_sub.name == "Hello :)""#
            );
        });
    }

    #[cfg(not(GraalPy))]
    #[pyclass(extends=PyDict)]
    #[derive(Debug)]
    struct DictWithName {
        #[pyo3(get, name = "name")]
        _name: &'static str,
    }

    #[cfg(not(GraalPy))]
    #[pymethods]
    impl DictWithName {
        #[new]
        fn new() -> Self {
            DictWithName { _name: "Hello :)" }
        }
    }

    #[cfg(not(GraalPy))]
    #[test]
    fn inherit_dict() {
        Python::attach(|py| {
            let dict_sub = pyo3::Py::new(py, DictWithName::new()).unwrap();
            py_run!(
                py,
                dict_sub,
                r#"dict_sub[0] = 1; assert dict_sub[0] == 1; assert dict_sub.name == "Hello :)""#
            );
        });
    }

    #[cfg(not(GraalPy))]
    #[test]
    fn inherit_dict_drop() {
        Python::attach(|py| {
            let dropped = Arc::new(AtomicBool::new(false));
            let destructor_drop = Arc::clone(&dropped);
            let item = PyCapsule::new_with_value_and_destructor(
                py,
                0,
                c"inherit_dict_drop",
                move |_, _| destructor_drop.store(true, Ordering::Relaxed),
            )
            .unwrap();

            let dict_sub = pyo3::Py::new(py, DictWithName::new()).unwrap();
            dict_sub.bind(py).set_item("foo", &item).unwrap();
            drop(item);
            assert!(!dropped.load(Ordering::Relaxed));
            drop(dict_sub);
            assert!(dropped.load(Ordering::Relaxed));
        })
    }

    #[pyclass(extends=PyException)]
    struct CustomException {
        #[pyo3(get)]
        context: &'static str,
    }

    #[pymethods]
    impl CustomException {
        #[new]
        fn new(_exc_arg: &Bound<'_, PyAny>) -> Self {
            CustomException {
                context: "Hello :)",
            }
        }
    }

    #[test]
    fn custom_exception() {
        Python::attach(|py| {
            let cls = py.get_type::<CustomException>();
            let dict = [("cls", &cls)].into_py_dict(py).unwrap();
            let res = py.run(
            c"e = cls('hello'); assert str(e) == 'hello'; assert e.context == 'Hello :)'; raise e",
            None,
            Some(&dict)
            );
            let err = res.unwrap_err();
            assert!(err.matches(py, &cls).unwrap(), "{}", err);

            // catching the exception in Python also works:
            py_run!(
                py,
                cls,
                r#"
                    try:
                        raise cls("foo")
                    except cls:
                        pass
                "#
            )
        })
    }

    #[cfg(Py_3_12)]
    #[pyclass(extends=pyo3::types::PyTzInfo)]
    struct TzInfoWithName {
        #[pyo3(get)]
        name: &'static str,
    }

    #[cfg(Py_3_12)]
    #[pymethods]
    impl TzInfoWithName {
        #[new]
        fn new() -> Self {
            Self { name: "Hello :)" }
        }

        #[pyo3(signature = (_dt, /))]
        fn utcoffset<'py>(
            &self,
            _dt: Option<&Bound<'_, pyo3::types::PyDateTime>>,
            py: Python<'py>,
        ) -> PyResult<Bound<'py, pyo3::types::PyDelta>> {
            pyo3::types::PyDelta::new(py, 0, 3600, 0, true)
        }
    }

    #[cfg(Py_3_12)]
    #[test]
    fn inherit_tzinfo() {
        Python::attach(|py| {
            let tz = pyo3::Py::new(py, TzInfoWithName::new()).unwrap();
            py_run!(
                py,
                tz,
                r#"
                    import datetime

                    assert isinstance(tz, datetime.tzinfo)
                    assert tz.name == "Hello :)"

                    dt = datetime.datetime(2024, 1, 1, tzinfo=tz)
                    assert dt.utcoffset() == datetime.timedelta(hours=1)
                "#
            );
        });
    }

    #[cfg(Py_3_12)]
    #[pyclass(extends=pyo3::types::PyList, subclass)]
    struct ListWithName {
        #[pyo3(get)]
        name: &'static str,
    }

    #[cfg(Py_3_12)]
    #[pymethods]
    impl ListWithName {
        #[new]
        fn new() -> Self {
            Self { name: "Hello :)" }
        }
    }

    #[cfg(Py_3_12)]
    #[pyclass(extends=ListWithName)]
    struct SubListWithName {
        #[pyo3(get)]
        sub_name: &'static str,
    }

    #[cfg(Py_3_12)]
    #[pymethods]
    impl SubListWithName {
        #[new]
        fn new() -> PyClassInitializer<Self> {
            PyClassInitializer::from(ListWithName::new()).add_subclass(Self {
                sub_name: "Sublist",
            })
        }
    }

    #[cfg(Py_3_12)]
    #[test]
    fn inherit_list() {
        Python::attach(|py| {
            let list_with_name = pyo3::Bound::new(py, ListWithName::new()).unwrap();
            let sub_list_with_name = pyo3::Bound::new(py, SubListWithName::new()).unwrap();
            py_run!(
                py,
                list_with_name sub_list_with_name,
                r#"
                    list_with_name.append(1)
                    assert list_with_name[0] == 1
                    assert list_with_name.name == "Hello :)", list_with_name.name

                    sub_list_with_name.append(1)
                    assert sub_list_with_name[0] == 1
                    assert sub_list_with_name.name == "Hello :)", sub_list_with_name.name
                    assert sub_list_with_name.sub_name == "Sublist", sub_list_with_name.sub_name
                "#
            );
        });
    }

    // Refcount tests for native type classes
    #[cfg(not(any(PyPy, GraalPy, Py_GIL_DISABLED)))]
    #[test]
    fn test_setwitname_ref_counts() {
        assert_type_refcount_stable!(SetWithName);
    }

    #[cfg(not(any(GraalPy, Py_GIL_DISABLED)))]
    #[test]
    fn test_dictwithname_ref_counts() {
        assert_type_refcount_stable!(DictWithName);
    }

    #[cfg(not(Py_GIL_DISABLED))]
    #[test]
    fn test_customexception_ref_counts() {
        assert_type_refcount_stable!(CustomException, "custom_exception", r#"Type('test')"#);
    }

    #[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))]
    #[test]
    fn test_tzinfowithname_ref_counts() {
        assert_type_refcount_stable!(TzInfoWithName);
    }

    #[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))]
    #[test]
    fn test_listwithname_ref_counts() {
        assert_type_refcount_stable!(ListWithName);
    }

    #[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))]
    #[test]
    fn test_sublistwithname_ref_counts() {
        assert_type_refcount_stable!(SubListWithName);
    }
}

#[pyclass(subclass)]
struct SimpleClass {}

#[pymethods]
impl SimpleClass {
    #[new]
    fn new() -> Self {
        Self {}
    }
}

// Generate refcount tests for all top-level types
#[cfg(not(Py_GIL_DISABLED))]
#[test]
fn test_baseclass_ref_counts() {
    assert_type_refcount_stable!(BaseClass);
}

#[cfg(not(Py_GIL_DISABLED))]
#[test]
fn test_subclass_ref_counts() {
    assert_type_refcount_stable!(SubClass);
}

#[cfg(not(Py_GIL_DISABLED))]
#[test]
fn test_base_class_with_result_ref_counts() {
    assert_type_refcount_stable!(BaseClassWithResult, "base_class_with_result", "Type(10)");
}

#[cfg(not(Py_GIL_DISABLED))]
#[test]
fn test_subclass2_ref_counts() {
    assert_type_refcount_stable!(SubClass2, "subclass2", "Type(10)");
}

#[cfg(not(Py_GIL_DISABLED))]
#[test]
fn test_simpleclass_ref_counts() {
    assert_type_refcount_stable!(SimpleClass);
}
