import sys
import gc
import unittest
import warnings
import weakref
import platform

import pytest

from gi.repository import GObject, GLib, Gio
from gi import PyGIDeprecationWarning
from gi.module import get_introspection_module
from gi import _gi

import testhelper
from .helper import capture_glib_deprecation_warnings


@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="crashes")
def test_gobject_weak_ref():
    called = []

    def callback(*args):
        called.extend(args)

    # object gets finalized
    obj = GObject.Object()
    obj.weak_ref(callback, 1)
    del obj
    gc.collect()
    assert called == [1]
    del called[:]

    # wrapper gets finalized first
    obj = GObject.Object()
    pyref = weakref.ref(obj, lambda x: callback(-2))
    value = GObject.Value(GObject.Object, obj)
    ref = obj.weak_ref(callback, 2)
    del obj
    gc.collect()
    assert called == [-2]
    del pyref
    value.unset()
    gc.collect()
    assert called == [-2, 2]
    del called[:]

    # weakref gets unregistered first
    obj = GObject.Object()
    ref = obj.weak_ref(callback, 3)
    ref.unref()
    del obj
    gc.collect()
    assert not called

    # weakref gets GCed
    obj = GObject.Object()
    obj.weak_ref(callback, 4)
    gc.collect()
    del obj
    assert called == [4]


class TestGObjectAPI(unittest.TestCase):
    def test_run_dispose(self):
        class TestObject(GObject.GObject):
            int_prop = GObject.Property(default=0, type=int)

        obj = TestObject()
        called = []

        def on_notify(*args):
            called.append(args)

        obj.connect("notify::int-prop", on_notify)
        obj.notify("int-prop")
        obj.notify("int-prop")
        # after this everything should be disconnected
        obj.run_dispose()
        obj.notify("int-prop")
        obj.notify("int-prop")
        assert len(called) == 2

    def test_call_method_uninitialized_instance(self):
        obj = GObject.Object.__new__(GObject.Object)
        with self.assertRaisesRegex(RuntimeError, ".*is not initialized"):
            obj.notify("foo")

    def test_gobject_inheritance(self):
        # GObject.Object is a class hierarchy as follows:
        # overrides.Object -> introspection.Object -> static.GObject
        GIObjectModule = get_introspection_module("GObject")
        self.assertTrue(issubclass(GObject.Object, GIObjectModule.Object))
        self.assertTrue(issubclass(GIObjectModule.Object, _gi.GObject))

        self.assertEqual(_gi.GObject.__gtype__, GObject.TYPE_OBJECT)
        self.assertEqual(GIObjectModule.Object.__gtype__, GObject.TYPE_OBJECT)
        self.assertEqual(GObject.Object.__gtype__, GObject.TYPE_OBJECT)

        # The pytype wrapper should hold the outer most Object class from overrides.
        self.assertEqual(GObject.TYPE_OBJECT.pytype, GObject.Object)

    def test_gobject_unsupported_overrides(self):
        obj = GObject.Object()

        with self.assertRaisesRegex(
            RuntimeError, "Data access methods are unsupported.*"
        ):
            obj.get_data()

        with self.assertRaisesRegex(
            RuntimeError, "This method is currently unsupported.*"
        ):
            obj.force_floating()

    def test_compat_api(self):
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            # GObject formerly exposed a lot of GLib's functions
            self.assertEqual(GObject.markup_escape_text("foo"), "foo")

            ml = GObject.MainLoop()
            self.assertFalse(ml.is_running())

            context = GObject.main_context_default()
            self.assertTrue(context.pending() in [False, True])

            context = GObject.MainContext()
            self.assertFalse(context.pending())

            self.assertTrue(issubclass(w[0].category, PyGIDeprecationWarning))
            self.assertTrue("GLib.markup_escape_text" in str(w[0]), str(w[0]))

            self.assertLess(GObject.PRIORITY_HIGH, GObject.PRIORITY_DEFAULT)

    def test_min_max_int(self):
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", PyGIDeprecationWarning)

            self.assertEqual(GObject.G_MAXINT16, 2**15 - 1)
            self.assertEqual(GObject.G_MININT16, -(2**15))
            self.assertEqual(GObject.G_MAXUINT16, 2**16 - 1)

            self.assertEqual(GObject.G_MAXINT32, 2**31 - 1)
            self.assertEqual(GObject.G_MININT32, -(2**31))
            self.assertEqual(GObject.G_MAXUINT32, 2**32 - 1)

            self.assertEqual(GObject.G_MAXINT64, 2**63 - 1)
            self.assertEqual(GObject.G_MININT64, -(2**63))
            self.assertEqual(GObject.G_MAXUINT64, 2**64 - 1)


class TestReferenceCounting(unittest.TestCase):
    def test_regular_object(self):
        obj = GObject.GObject()
        self.assertEqual(obj.__grefcount__, 1)

        obj = GObject.new(GObject.GObject)
        self.assertEqual(obj.__grefcount__, 1)

    def test_floating(self):
        obj = testhelper.Floating()
        self.assertEqual(obj.__grefcount__, 1)

        obj = GObject.new(testhelper.Floating)
        self.assertEqual(obj.__grefcount__, 1)

    def test_owned_by_library(self):
        # Upon creation, the refcount of the object should be 2:
        # - someone already has a reference on the new object.
        # - the python wrapper should hold its own reference.
        obj = testhelper.OwnedByLibrary()
        self.assertEqual(obj.__grefcount__, 2)

        # We ask the library to release its reference, so the only
        # remaining ref should be our wrapper's. Once the wrapper
        # will run out of scope, the object will get finalized.
        obj.release()
        self.assertEqual(obj.__grefcount__, 1)

    def test_owned_by_library_out_of_scope(self):
        obj = testhelper.OwnedByLibrary()
        self.assertEqual(obj.__grefcount__, 2)

        # We are manually taking the object out of scope. This means
        # that our wrapper has been freed, and its reference dropped. We
        # cannot check it but the refcount should now be 1 (the ref held
        # by the library is still there, we didn't call release()
        obj = None

        # When we get the object back from the lib, the wrapper is
        # re-created, so our refcount will be 2 once again.
        obj = testhelper.owned_by_library_get_instance_list()[0]
        self.assertEqual(obj.__grefcount__, 2)

        obj.release()
        self.assertEqual(obj.__grefcount__, 1)

    def test_owned_by_library_using_gobject_new(self):
        # Upon creation, the refcount of the object should be 2:
        # - someone already has a reference on the new object.
        # - the python wrapper should hold its own reference.
        obj = GObject.new(testhelper.OwnedByLibrary)
        self.assertEqual(obj.__grefcount__, 2)

        # We ask the library to release its reference, so the only
        # remaining ref should be our wrapper's. Once the wrapper
        # will run out of scope, the object will get finalized.
        obj.release()
        self.assertEqual(obj.__grefcount__, 1)

    def test_owned_by_library_out_of_scope_using_gobject_new(self):
        obj = GObject.new(testhelper.OwnedByLibrary)
        self.assertEqual(obj.__grefcount__, 2)

        # We are manually taking the object out of scope. This means
        # that our wrapper has been freed, and its reference dropped. We
        # cannot check it but the refcount should now be 1 (the ref held
        # by the library is still there, we didn't call release()
        obj = None

        # When we get the object back from the lib, the wrapper is
        # re-created, so our refcount will be 2 once again.
        obj = testhelper.owned_by_library_get_instance_list()[0]
        self.assertEqual(obj.__grefcount__, 2)

        obj.release()
        self.assertEqual(obj.__grefcount__, 1)

    def test_floating_and_sunk(self):
        # Upon creation, the refcount of the object should be 2:
        # - someone already has a reference on the new object.
        # - the python wrapper should hold its own reference.
        obj = testhelper.FloatingAndSunk()
        self.assertEqual(obj.__grefcount__, 2)

        # We ask the library to release its reference, so the only
        # remaining ref should be our wrapper's. Once the wrapper
        # will run out of scope, the object will get finalized.
        obj.release()
        self.assertEqual(obj.__grefcount__, 1)

    def test_floating_and_sunk_out_of_scope(self):
        obj = testhelper.FloatingAndSunk()
        self.assertEqual(obj.__grefcount__, 2)

        # We are manually taking the object out of scope. This means
        # that our wrapper has been freed, and its reference dropped. We
        # cannot check it but the refcount should now be 1 (the ref held
        # by the library is still there, we didn't call release()
        obj = None

        # When we get the object back from the lib, the wrapper is
        # re-created, so our refcount will be 2 once again.
        obj = testhelper.floating_and_sunk_get_instance_list()[0]
        self.assertEqual(obj.__grefcount__, 2)

        obj.release()
        self.assertEqual(obj.__grefcount__, 1)

    def test_floating_and_sunk_using_gobject_new(self):
        # Upon creation, the refcount of the object should be 2:
        # - someone already has a reference on the new object.
        # - the python wrapper should hold its own reference.
        obj = GObject.new(testhelper.FloatingAndSunk)
        self.assertEqual(obj.__grefcount__, 2)

        # We ask the library to release its reference, so the only
        # remaining ref should be our wrapper's. Once the wrapper
        # will run out of scope, the object will get finalized.
        obj.release()
        self.assertEqual(obj.__grefcount__, 1)

    def test_floating_and_sunk_out_of_scope_using_gobject_new(self):
        obj = GObject.new(testhelper.FloatingAndSunk)
        self.assertEqual(obj.__grefcount__, 2)

        # We are manually taking the object out of scope. This means
        # that our wrapper has been freed, and its reference dropped. We
        # cannot check it but the refcount should now be 1 (the ref held
        # by the library is still there, we didn't call release()
        obj = None

        # When we get the object back from the lib, the wrapper is
        # re-created, so our refcount will be 2 once again.
        obj = testhelper.floating_and_sunk_get_instance_list()[0]
        self.assertEqual(obj.__grefcount__, 2)

        obj.release()
        self.assertEqual(obj.__grefcount__, 1)

    def test_uninitialized_object(self):
        class Obj(GObject.GObject):
            def __init__(self):
                x = self.__grefcount__
                super().__init__()
                assert x >= 0

        # Accessing __grefcount__ before the object is initialized is wrong.
        # Ensure we get a proper exception instead of a crash.
        self.assertRaises(TypeError, Obj)


class A(GObject.GObject):
    def __init__(self):
        super().__init__()


class TestPythonReferenceCounting(unittest.TestCase):
    # Newly created instances should always have two references: one for
    # the GC, and one for the bound variable in the local scope.

    def test_new_instance_has_two_refs(self):
        obj = GObject.GObject()
        if hasattr(sys, "getrefcount"):
            self.assertEqual(
                sys.getrefcount(obj), 2 if sys.version_info < (3, 14) else 1
            )

    def test_new_instance_has_two_refs_using_gobject_new(self):
        obj = GObject.new(GObject.GObject)
        if hasattr(sys, "getrefcount"):
            self.assertEqual(
                sys.getrefcount(obj), 2 if sys.version_info < (3, 14) else 1
            )

    def test_new_subclass_instance_has_two_refs(self):
        obj = A()
        if hasattr(sys, "getrefcount"):
            self.assertEqual(
                sys.getrefcount(obj), 2 if sys.version_info < (3, 14) else 1
            )

    def test_new_subclass_instance_has_two_refs_using_gobject_new(self):
        obj = GObject.new(A)
        if hasattr(sys, "getrefcount"):
            self.assertEqual(
                sys.getrefcount(obj), 2 if sys.version_info < (3, 14) else 1
            )


class TestContextManagers(unittest.TestCase):
    class ContextTestObject(GObject.GObject):
        prop = GObject.Property(default=0, type=int)

    def on_prop_set(self, obj, prop):
        # Handler which tracks property changed notifications.
        self.tracking.append(obj.get_property(prop.name))

    def setUp(self):
        self.tracking = []
        self.obj = self.ContextTestObject()
        self.handler = self.obj.connect("notify::prop", self.on_prop_set)

    def test_freeze_notify_context(self):
        # Verify prop tracking list
        self.assertEqual(self.tracking, [])
        self.obj.props.prop = 1
        self.assertEqual(self.tracking, [1])
        self.obj.props.prop = 2
        self.assertEqual(self.tracking, [1, 2])
        self.assertEqual(self.obj.__grefcount__, 1)

        if hasattr(sys, "getrefcount"):
            pyref_count = sys.getrefcount(self.obj)

        # Using the context manager the tracking list should not be affected.
        # The GObject reference count should stay the same and the python
        # object ref-count should go up.
        with self.obj.freeze_notify():
            self.assertEqual(self.obj.__grefcount__, 1)
            if hasattr(sys, "getrefcount"):
                self.assertEqual(sys.getrefcount(self.obj), pyref_count + 1)
            self.obj.props.prop = 3
            self.assertEqual(self.obj.props.prop, 3)
            self.assertEqual(self.tracking, [1, 2])

        # After the context manager, the prop should have been modified,
        # the tracking list will be modified, and the python object ref
        # count goes back down.
        gc.collect()
        self.assertEqual(self.obj.props.prop, 3)
        self.assertEqual(self.tracking, [1, 2, 3])
        self.assertEqual(self.obj.__grefcount__, 1)
        if hasattr(sys, "getrefcount"):
            self.assertEqual(sys.getrefcount(self.obj), pyref_count)

    def test_handler_block_context(self):
        # Verify prop tracking list
        self.assertEqual(self.tracking, [])
        self.obj.props.prop = 1
        self.assertEqual(self.tracking, [1])
        self.obj.props.prop = 2
        self.assertEqual(self.tracking, [1, 2])
        self.assertEqual(self.obj.__grefcount__, 1)

        if hasattr(sys, "getrefcount"):
            pyref_count = sys.getrefcount(self.obj)

        # Using the context manager the tracking list should not be affected.
        # The GObject reference count should stay the same and the python
        # object ref-count should go up.
        with self.obj.handler_block(self.handler):
            self.assertEqual(self.obj.__grefcount__, 1)
            if hasattr(sys, "getrefcount"):
                self.assertEqual(sys.getrefcount(self.obj), pyref_count + 1)
            self.obj.props.prop = 3
            self.assertEqual(self.obj.props.prop, 3)
            self.assertEqual(self.tracking, [1, 2])

        # After the context manager, the prop should have been modified
        # the tracking list should have stayed the same and the GObject ref
        # count goes back down.
        gc.collect()
        self.assertEqual(self.obj.props.prop, 3)
        self.assertEqual(self.tracking, [1, 2])
        self.assertEqual(self.obj.__grefcount__, 1)
        if hasattr(sys, "getrefcount"):
            self.assertEqual(sys.getrefcount(self.obj), pyref_count)

    def test_freeze_notify_context_nested(self):
        self.assertEqual(self.tracking, [])
        with self.obj.freeze_notify():
            self.obj.props.prop = 1
            self.assertEqual(self.tracking, [])

            with self.obj.freeze_notify():
                self.obj.props.prop = 2
                self.assertEqual(self.tracking, [])

                with self.obj.freeze_notify():
                    self.obj.props.prop = 3
                    self.assertEqual(self.tracking, [])
                self.assertEqual(self.tracking, [])
            self.assertEqual(self.tracking, [])

        # Finally after last context, the notifications should have collapsed
        # and the last one sent.
        self.assertEqual(self.tracking, [3])

    def test_handler_block_context_nested(self):
        self.assertEqual(self.tracking, [])
        with self.obj.handler_block(self.handler):
            self.obj.props.prop = 1
            self.assertEqual(self.tracking, [])

            with self.obj.handler_block(self.handler):
                self.obj.props.prop = 2
                self.assertEqual(self.tracking, [])

                with self.obj.handler_block(self.handler):
                    self.obj.props.prop = 3
                    self.assertEqual(self.tracking, [])
                self.assertEqual(self.tracking, [])
            self.assertEqual(self.tracking, [])

        # Finally after last context, the notifications should have collapsed
        # and the last one sent.
        self.assertEqual(self.obj.props.prop, 3)
        self.assertEqual(self.tracking, [])

    def test_freeze_notify_normal_usage_ref_counts(self):
        # Ensure ref counts without using methods as context managers
        # maintain the same count.
        self.assertEqual(self.obj.__grefcount__, 1)
        self.obj.freeze_notify()
        self.assertEqual(self.obj.__grefcount__, 1)
        self.obj.thaw_notify()
        self.assertEqual(self.obj.__grefcount__, 1)

    def test_handler_block_normal_usage_ref_counts(self):
        self.assertEqual(self.obj.__grefcount__, 1)
        self.obj.handler_block(self.handler)
        self.assertEqual(self.obj.__grefcount__, 1)
        self.obj.handler_unblock(self.handler)
        self.assertEqual(self.obj.__grefcount__, 1)

    def test_freeze_notify_context_error(self):
        # Test an exception occurring within a freeze context exits the context
        try:
            with self.obj.freeze_notify():
                self.obj.props.prop = 1
                self.assertEqual(self.tracking, [])
                raise ValueError("Simulation")
        except ValueError:
            pass

        # Verify the property set within the context called notify.
        self.assertEqual(self.obj.props.prop, 1)
        self.assertEqual(self.tracking, [1])

        # Verify we are still not in a frozen context.
        self.obj.props.prop = 2
        self.assertEqual(self.tracking, [1, 2])

    def test_handler_block_context_error(self):
        # Test an exception occurring within a handler block exits the context
        try:
            with self.obj.handler_block(self.handler):
                self.obj.props.prop = 1
                self.assertEqual(self.tracking, [])
                raise ValueError("Simulation")
        except ValueError:
            pass

        # Verify the property set within the context didn't call notify.
        self.assertEqual(self.obj.props.prop, 1)
        self.assertEqual(self.tracking, [])

        # Verify we are still not in a handler block context.
        self.obj.props.prop = 2
        self.assertEqual(self.tracking, [2])


class TestPropertyBindings(unittest.TestCase):
    class TestObject(GObject.GObject):
        int_prop = GObject.Property(default=0, type=int)

    def setUp(self):
        self.source = self.TestObject()
        self.target = self.TestObject()

    def test_default_binding(self):
        binding = self.source.bind_property(
            "int_prop", self.target, "int_prop", GObject.BindingFlags.DEFAULT
        )
        binding = binding

        # Test setting value on source gets pushed to target
        self.source.int_prop = 1
        self.assertEqual(self.source.int_prop, 1)
        self.assertEqual(self.target.int_prop, 1)

        # Test setting value on target does not change source
        self.target.props.int_prop = 2
        self.assertEqual(self.source.int_prop, 1)
        self.assertEqual(self.target.int_prop, 2)

    def test_call_binding(self):
        binding = self.source.bind_property(
            "int_prop", self.target, "int_prop", GObject.BindingFlags.DEFAULT
        )
        with capture_glib_deprecation_warnings() as warn:
            result = binding()
        assert len(warn)
        assert result is binding

    def test_bidirectional_binding(self):
        binding = self.source.bind_property(
            "int_prop", self.target, "int_prop", GObject.BindingFlags.BIDIRECTIONAL
        )
        binding = binding

        # Test setting value on source gets pushed to target
        self.source.int_prop = 1
        self.assertEqual(self.source.int_prop, 1)
        self.assertEqual(self.target.int_prop, 1)

        # Test setting value on target also changes source
        self.target.props.int_prop = 2
        self.assertEqual(self.source.int_prop, 2)
        self.assertEqual(self.target.int_prop, 2)

    def test_transform_to_only(self):
        def transform_to(binding, value, user_data=None):
            self.assertEqual(user_data, "test-data")
            return value * 2

        binding = self.source.bind_property(
            "int_prop",
            self.target,
            "int_prop",
            GObject.BindingFlags.DEFAULT,
            transform_to,
            user_data="test-data",
        )
        binding = binding

        self.source.int_prop = 1
        self.assertEqual(self.source.int_prop, 1)
        self.assertEqual(self.target.int_prop, 2)

        self.target.props.int_prop = 1
        self.assertEqual(self.source.int_prop, 1)
        self.assertEqual(self.target.int_prop, 1)

    def test_transform_from_only(self):
        def transform_from(binding, value, user_data=None):
            self.assertEqual(user_data, None)
            return value * 2

        binding = self.source.bind_property(
            "int_prop",
            self.target,
            "int_prop",
            GObject.BindingFlags.BIDIRECTIONAL,
            transform_from=transform_from,
        )
        binding = binding

        self.source.int_prop = 1
        self.assertEqual(self.source.int_prop, 1)
        self.assertEqual(self.target.int_prop, 1)

        self.target.props.int_prop = 1
        self.assertEqual(self.source.int_prop, 2)
        self.assertEqual(self.target.int_prop, 1)

    def test_transform_bidirectional(self):
        test_data = object()

        def transform_to(binding, value, user_data=None):
            self.assertEqual(user_data, test_data)
            return value * 2

        def transform_from(binding, value, user_data=None):
            self.assertEqual(user_data, test_data)
            return value // 2

        if hasattr(sys, "getrefcount"):
            test_data_ref_count = sys.getrefcount(test_data)
            transform_to_ref_count = sys.getrefcount(transform_to)
            transform_from_ref_count = sys.getrefcount(transform_from)

        # bidirectional bindings
        binding = self.source.bind_property(
            "int_prop",
            self.target,
            "int_prop",
            GObject.BindingFlags.BIDIRECTIONAL,
            transform_to,
            transform_from,
            test_data,
        )
        binding = binding
        if hasattr(sys, "getrefcount"):
            binding_ref_count = sys.getrefcount(binding)
            binding_gref_count = binding.__grefcount__

        self.source.int_prop = 1
        self.assertEqual(self.source.int_prop, 1)
        self.assertEqual(self.target.int_prop, 2)

        self.target.props.int_prop = 4
        self.assertEqual(self.source.int_prop, 2)
        self.assertEqual(self.target.int_prop, 4)

        if hasattr(sys, "getrefcount"):
            self.assertEqual(sys.getrefcount(binding), binding_ref_count)
            self.assertEqual(binding.__grefcount__, binding_gref_count)

        # test_data ref count increases by 2, once for each callback.
        if hasattr(sys, "getrefcount"):
            self.assertEqual(sys.getrefcount(test_data), test_data_ref_count + 2)
            self.assertEqual(sys.getrefcount(transform_to), transform_to_ref_count + 1)
            self.assertEqual(
                sys.getrefcount(transform_from), transform_from_ref_count + 1
            )

        # Unbind should clear out the binding and its transforms
        binding.unbind()

        # Setting source or target should not change the other.
        self.target.int_prop = 3
        self.source.int_prop = 5
        self.assertEqual(self.target.int_prop, 3)
        self.assertEqual(self.source.int_prop, 5)

        if hasattr(sys, "getrefcount"):
            self.assertEqual(sys.getrefcount(test_data), test_data_ref_count)
            self.assertEqual(sys.getrefcount(transform_to), transform_to_ref_count)
            self.assertEqual(sys.getrefcount(transform_from), transform_from_ref_count)

    def test_explicit_unbind_clears_connection(self):
        self.assertEqual(self.source.int_prop, 0)
        self.assertEqual(self.target.int_prop, 0)

        # Test deleting binding reference removes binding.
        binding = self.source.bind_property("int_prop", self.target, "int_prop")
        self.source.int_prop = 1
        self.assertEqual(self.source.int_prop, 1)
        self.assertEqual(self.target.int_prop, 1)

        # unbind should clear out the bindings self reference
        binding.unbind()
        self.assertEqual(binding.__grefcount__, 1)

        self.source.int_prop = 10
        self.assertEqual(self.source.int_prop, 10)
        self.assertEqual(self.target.int_prop, 1)

        for i in range(10):
            binding.unbind()

    def test_reference_counts(self):
        self.assertEqual(self.source.__grefcount__, 1)
        self.assertEqual(self.target.__grefcount__, 1)

        # Binding ref count will be 2 do to the initial ref implicitly held by
        # the act of binding and the ref incurred by using __call__ to generate
        # a wrapper from the weak binding ref within python.
        binding = self.source.bind_property("int_prop", self.target, "int_prop")
        self.assertEqual(binding.__grefcount__, 2)

        # Creating a binding does not inc refs on source and target (they are weak
        # on the binding object itself)
        self.assertEqual(self.source.__grefcount__, 1)
        self.assertEqual(self.target.__grefcount__, 1)

        # Use GObject.get_property because the "props" accessor leaks.
        # Note property names are canonicalized.
        self.assertEqual(binding.get_property("source"), self.source)
        self.assertEqual(binding.get_property("source_property"), "int-prop")
        self.assertEqual(binding.get_property("target"), self.target)
        self.assertEqual(binding.get_property("target_property"), "int-prop")
        self.assertEqual(binding.get_property("flags"), GObject.BindingFlags.DEFAULT)

        # Delete reference to source or target and the binding will remove its own
        # "self reference".
        ref = self.source.weak_ref()
        del self.source
        gc.collect()
        self.assertEqual(ref(), None)
        self.assertEqual(binding.__grefcount__, 1)

        # Finally clear out the last ref held by the python wrapper
        ref = binding.weak_ref()
        del binding
        gc.collect()
        self.assertEqual(ref(), None)


class TestGValue(unittest.TestCase):
    def test_type_constant(self):
        self.assertEqual(GObject.TYPE_VALUE, GObject.Value.__gtype__)
        self.assertEqual(GObject.type_name(GObject.TYPE_VALUE), "GValue")

    def test_no_type(self):
        value = GObject.Value()
        self.assertEqual(value.g_type, GObject.TYPE_INVALID)
        self.assertRaises(TypeError, value.set_value, 23)
        self.assertEqual(value.get_value(), None)

    def test_int(self):
        value = GObject.Value(GObject.TYPE_UINT)
        self.assertEqual(value.g_type, GObject.TYPE_UINT)
        value.set_value(23)
        self.assertEqual(value.get_value(), 23)
        value.set_value(42.0)
        self.assertEqual(value.get_value(), 42)

    def test_string(self):
        value = GObject.Value(str, "foo_bar")
        self.assertEqual(value.g_type, GObject.TYPE_STRING)
        self.assertEqual(value.get_value(), "foo_bar")

    def test_float(self):
        # python float is G_TYPE_DOUBLE
        value = GObject.Value(float, 23.4)
        self.assertEqual(value.g_type, GObject.TYPE_DOUBLE)
        value.set_value(1e50)
        self.assertAlmostEqual(value.get_value(), 1e50)

        value = GObject.Value(GObject.TYPE_FLOAT, 23.4)
        self.assertEqual(value.g_type, GObject.TYPE_FLOAT)
        self.assertRaises(TypeError, value.set_value, "string")
        self.assertRaises(OverflowError, value.set_value, 1e50)

    def test_float_inf_nan(self):
        nan = float("nan")
        for type_ in [GObject.TYPE_FLOAT, GObject.TYPE_DOUBLE]:
            for x in [float("inf"), float("-inf"), nan]:
                value = GObject.Value(type_, x)
                # assertEqual() is False for (nan, nan)
                if x is nan:
                    self.assertEqual(str(value.get_value()), "nan")
                else:
                    self.assertEqual(value.get_value(), x)

    def test_enum(self):
        value = GObject.Value(Gio.FileType, Gio.FileType.DIRECTORY)
        self.assertEqual(value.get_value(), Gio.FileType.DIRECTORY)

    def test_flags(self):
        value = GObject.Value(Gio.FileCopyFlags, Gio.FileCopyFlags.OVERWRITE)
        self.assertEqual(value.get_value(), Gio.FileCopyFlags.OVERWRITE)

    def test_object(self):
        class TestObject(GObject.Object):
            pass

        obj = TestObject()
        value = GObject.Value(GObject.TYPE_OBJECT, obj)
        self.assertEqual(value.get_value(), obj)

    def test_value_array(self):
        value = GObject.Value(GObject.ValueArray)
        self.assertEqual(value.g_type, GObject.type_from_name("GValueArray"))
        value.set_value([32, "foo_bar", 0.3])
        self.assertEqual(value.get_value(), [32, "foo_bar", 0.3])

    def test_value_array_from_gvalue_list(self):
        value = GObject.Value(
            GObject.ValueArray,
            [
                GObject.Value(GObject.TYPE_UINT, 0xFFFFFFFF),
                GObject.Value(GObject.TYPE_STRING, "foo_bar"),
            ],
        )
        self.assertEqual(value.g_type, GObject.type_from_name("GValueArray"))
        self.assertEqual(value.get_value(), [0xFFFFFFFF, "foo_bar"])
        self.assertEqual(
            testhelper.value_array_get_nth_type(value, 0), GObject.TYPE_UINT
        )
        self.assertEqual(
            testhelper.value_array_get_nth_type(value, 1), GObject.TYPE_STRING
        )

    def test_value_array_append_gvalue(self):
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", DeprecationWarning)

            arr = GObject.ValueArray.new(0)
            arr.append(GObject.Value(GObject.TYPE_UINT, 0xFFFFFFFF))
            arr.append(GObject.Value(GObject.TYPE_STRING, "foo_bar"))
            self.assertEqual(arr.get_nth(0), 0xFFFFFFFF)
            self.assertEqual(arr.get_nth(1), "foo_bar")
            self.assertEqual(
                testhelper.value_array_get_nth_type(arr, 0), GObject.TYPE_UINT
            )
            self.assertEqual(
                testhelper.value_array_get_nth_type(arr, 1), GObject.TYPE_STRING
            )

    def test_gerror_boxing(self):
        error = GLib.Error("test message", domain="mydomain", code=42)
        value = GObject.Value(GLib.Error, error)
        self.assertEqual(value.g_type, GObject.type_from_name("GError"))

        unboxed = value.get_value()
        self.assertEqual(unboxed.message, error.message)
        self.assertEqual(unboxed.domain, error.domain)
        self.assertEqual(unboxed.code, error.code)

    def test_gerror_novalue(self):
        GLib.Error("test message", domain="mydomain", code=42)
        value = GObject.Value(GLib.Error)
        self.assertEqual(value.g_type, GObject.type_from_name("GError"))
        self.assertEqual(value.get_value(), None)


class TestDispose(unittest.TestCase):
    def test_dispose(self):
        class TestObject(GObject.Object):
            def __init__(self):
                super().__init__()
                self.dispose_invoked = False

            def do_dispose(self):
                self.dispose_invoked = True
                super().do_dispose()

        obj = TestObject()
        obj.run_dispose()

        assert obj.dispose_invoked

    def test_dispose_with_python_base_class(self):
        class TestBaseObject(GObject.Object):
            def __init__(self):
                super().__init__()
                self.dispose_invoked_base = False

            def do_dispose(self):
                self.dispose_invoked_base = True
                super().do_dispose()

        class TestObject(TestBaseObject):
            def __init__(self):
                super().__init__()
                self.dispose_invoked = False

            def do_dispose(self):
                self.dispose_invoked = True
                super().do_dispose()

        obj = TestObject()
        obj.run_dispose()

        assert obj.dispose_invoked_base
        assert obj.dispose_invoked

    @unittest.skipIf(platform.python_implementation() == "PyPy", "CPython only")
    def test_dispose_on_object_destruction(self):
        dispose_invoked = False

        class TestObject(GObject.Object):
            def do_dispose(self):
                nonlocal dispose_invoked
                dispose_invoked = True
                super().do_dispose()

        TestObject()

        assert dispose_invoked

    @unittest.skipIf(platform.python_implementation() == "PyPy", "CPython only")
    def test_dispose_on_floating_object(self):
        dispose_invoked = False

        class TestObject(GObject.Object):
            def do_dispose(self):
                nonlocal dispose_invoked
                dispose_invoked = True
                super().do_dispose()

        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            TestObject()._force_floating()

        assert dispose_invoked

    def test_only_call_do_dispose_if_defined_on_class(self):
        dispose_invoked = False

        def do_dispose(self):
            nonlocal dispose_invoked
            dispose_invoked = True

        class TestObject(GObject.Object):
            def __init__(self):
                self.do_dispose = do_dispose
                super().__init__()

        TestObject()

        assert not dispose_invoked

    @unittest.skipIf(platform.python_implementation() == "PyPy", "CPython only")
    def test_access_members_objects_from_dispose(self):
        dispose_invoked = False

        class TestObject(GObject.Object):
            def __init__(self, a, b):
                super().__init__()
                self.a = a
                self.b = b

            def do_dispose(self):
                nonlocal dispose_invoked
                # access members: do_dispose should be called before they're removed
                self.a.val
                self.b.val
                dispose_invoked = True

        class TestMember:
            def __init__(self, val):
                self.val = val

        a = TestMember("a")
        b = TestMember("b")
        o = TestObject(a, b)
        a.o = o
        del a, b, o

        gc.collect()

        assert dispose_invoked


def test_list_properties():
    def find_param(props, name):
        for param in props:
            if param.name == name:
                return param
        return None

    list_props = GObject.list_properties

    props = list_props(Gio.Action)
    param = find_param(props, "enabled")
    assert param
    assert param.value_type == GObject.TYPE_BOOLEAN
    assert list_props("GAction") == list_props(Gio.Action)
    assert list_props(Gio.Action.__gtype__) == list_props(Gio.Action)

    props = list_props(Gio.SimpleAction)
    assert find_param(props, "enabled")

    def names(props):
        return [p.name for p in props]

    assert set(names(list_props(Gio.Action))) <= set(
        names(list_props(Gio.SimpleAction))
    )

    props = list_props(Gio.FileIcon)
    param = find_param(props, "file")
    assert param
    assert param.value_type == Gio.File.__gtype__

    assert list_props("GFileIcon") == list_props(Gio.FileIcon)
    assert list_props(Gio.FileIcon.__gtype__) == list_props(Gio.FileIcon)
    assert list_props(Gio.FileIcon(file=Gio.File.new_for_path("."))) == list_props(
        Gio.FileIcon
    )

    for obj in [Gio.ActionEntry, Gio.DBusError, 0, object()]:
        with pytest.raises(TypeError):
            list_props(obj)


def test_custom_class_update():
    # Example from Gaupol <https://github.com/otsaloma/gaupol>

    class CustomAction(Gio.SimpleAction):
        def __new__(cls):
            action = Gio.SimpleAction.new("custom-action")
            action.__class__ = cls
            return action

    action = CustomAction()
    assert type(action) is CustomAction

    group = Gio.SimpleActionGroup()
    group.insert(action)
    del action
    new_action = group.lookup("custom-action")

    assert type(new_action) is CustomAction
