from __future__ import annotations

import pytest

import env  # noqa: F401
import pybind11_tests.class_sh_trampoline_shared_ptr_cpp_arg as m


@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
def test_shared_ptr_cpp_arg():
    import weakref

    class PyChild(m.SpBase):
        def is_base_used(self):
            return False

    tester = m.SpBaseTester()

    obj = PyChild()
    objref = weakref.ref(obj)

    # Pass the last python reference to the C++ function
    tester.set_object(obj)
    del obj
    pytest.gc_collect()

    # python reference is still around since C++ has it now
    assert objref() is not None
    assert tester.is_base_used() is False
    assert tester.obj.is_base_used() is False
    assert tester.get_object() is objref()


@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
def test_shared_ptr_cpp_prop():
    class PyChild(m.SpBase):
        def is_base_used(self):
            return False

    tester = m.SpBaseTester()

    # Set the last python reference as a property of the C++ object
    tester.obj = PyChild()
    pytest.gc_collect()

    # python reference is still around since C++ has it now
    assert tester.is_base_used() is False
    assert tester.has_python_instance() is True
    assert tester.obj.is_base_used() is False
    assert tester.obj.has_python_instance() is True


@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
def test_shared_ptr_arg_identity():
    import weakref

    tester = m.SpBaseTester()

    obj = m.SpBase()
    objref = weakref.ref(obj)

    tester.set_object(obj)
    del obj
    pytest.gc_collect()

    # NOTE: the behavior below is DIFFERENT from PR #2839
    # python reference is gone because it is not an Alias instance
    assert objref() is None
    assert tester.has_python_instance() is False


@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
def test_shared_ptr_alias_nonpython():
    tester = m.SpBaseTester()

    # C++ creates the object, a python instance shouldn't exist
    tester.set_nonpython_instance()
    assert tester.is_base_used() is True
    assert tester.has_instance() is True
    assert tester.has_python_instance() is False

    # Now a python instance exists
    cobj = tester.get_object()
    assert cobj.has_python_instance()
    assert tester.has_instance() is True
    assert tester.has_python_instance() is True

    # Now it's gone
    del cobj
    pytest.gc_collect()
    assert tester.has_instance() is True
    assert tester.has_python_instance() is False

    # When we pass it as an arg to a new tester the python instance should
    # disappear because it wasn't created with an alias
    new_tester = m.SpBaseTester()

    cobj = tester.get_object()
    assert cobj.has_python_instance()

    new_tester.set_object(cobj)
    assert tester.has_python_instance() is True
    assert new_tester.has_python_instance() is True

    del cobj
    pytest.gc_collect()

    # Gone!
    assert tester.has_instance() is True
    assert tester.has_python_instance() is False
    assert new_tester.has_instance() is True
    assert new_tester.has_python_instance() is False


@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
def test_shared_ptr_goaway():
    import weakref

    tester = m.SpGoAwayTester()

    obj = m.SpGoAway()
    objref = weakref.ref(obj)

    assert tester.obj is None

    tester.obj = obj
    del obj
    pytest.gc_collect()

    # python reference is no longer around
    assert objref() is None
    # C++ reference is still around
    assert tester.obj is not None


def test_infinite():
    tester = m.SpBaseTester()
    while True:
        tester.set_object(m.SpBase())
        break  # Comment out for manual leak checking (use `top` command).


@pytest.mark.parametrize(
    "pass_through_func", [m.pass_through_shd_ptr, m.pass_through_shd_ptr_release_gil]
)
def test_std_make_shared_factory(pass_through_func):
    class PyChild(m.SpBase):
        def __init__(self):
            super().__init__(0)

    obj = PyChild()
    while True:
        assert pass_through_func(obj) is obj
        break  # Comment out for manual leak checking (use `top` command).
