import gc
import os
import tempfile
import weakref

import pytest

Gtk = pytest.importorskip("gi.repository.Gtk")
GLib = pytest.importorskip("gi.repository.GLib")
GObject = pytest.importorskip("gi.repository.GObject")
Gio = pytest.importorskip("gi.repository.Gio")

from .helper import capture_exceptions

GTK4 = Gtk._version == "4.0"


def new_gtype_name(_count=[0]):
    _count[0] += 1
    return f"GtkTemplateTest{_count[0]:d}"


def ensure_resource_registered():
    resource_path = "/org/gnome/pygobject/test/a.ui"

    def is_registered(path):
        try:
            Gio.resources_get_info(path, Gio.ResourceLookupFlags.NONE)
        except GLib.Error:
            return False
        return True

    if is_registered(resource_path):
        return resource_path

    gresource_data = (
        b"GVariant\x00\x00\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00"
        b"\xc8\x00\x00\x00\x00\x00\x00(\x06\x00\x00\x00\x00\x00\x00\x00"
        b"\x00\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x05\x00\x00\x00"
        b"\x06\x00\x00\x00KP\x90\x0b\x03\x00\x00\x00\xc8\x00\x00\x00"
        b"\x04\x00L\x00\xcc\x00\x00\x00\xd0\x00\x00\x00\xb0\xb7$0"
        b"\x00\x00\x00\x00\xd0\x00\x00\x00\x06\x00L\x00\xd8\x00\x00\x00"
        b"\xdc\x00\x00\x00f\xc30\xd1\x01\x00\x00\x00\xdc\x00\x00\x00"
        b"\n\x00L\x00\xe8\x00\x00\x00\xec\x00\x00\x00\xd4\xb5\x02\x00"
        b"\xff\xff\xff\xff\xec\x00\x00\x00\x01\x00L\x00\xf0\x00\x00\x00"
        b"\xf4\x00\x00\x005H}\xe3\x02\x00\x00\x00\xf4\x00\x00\x00"
        b"\x05\x00L\x00\xfc\x00\x00\x00\x00\x01\x00\x00\xa2^\xd6t"
        b"\x04\x00\x00\x00\x00\x01\x00\x00\x04\x00v\x00\x08\x01\x00\x00"
        b"\xa5\x01\x00\x00org/\x01\x00\x00\x00gnome/\x00\x00\x02\x00\x00\x00"
        b"pygobject/\x00\x00\x04\x00\x00\x00/\x00\x00\x00\x00\x00\x00\x00"
        b"test/\x00\x00\x00\x05\x00\x00\x00a.ui\x00\x00\x00\x00"
        b'\x8d\x00\x00\x00\x00\x00\x00\x00<interface>\n  <template class="G'
        b'tkTemplateTestResource" parent="GtkBox">\n  <property name="spaci'
        b'ng">42</property>\n  </template>\n</interface>\n\x00\x00(uuay)'
    )

    resource = Gio.Resource.new_from_data(GLib.Bytes.new(gresource_data))
    Gio.resources_register(resource)
    assert is_registered(resource_path)
    return resource_path


def test_allow_init_template_call():
    type_name = new_gtype_name()

    xml = f"""\
<interface>
  <template class="{type_name}" parent="GtkBox">
  </template>
</interface>
"""

    @Gtk.Template.from_string(xml)
    class Foo(Gtk.Box):
        __gtype_name__ = type_name

        def __init__(self):
            super().__init__()
            self.init_template()

    # Stop current pygobject from handling the initialisation
    del Foo.__dontuse_ginstance_init__

    Foo()


def test_init_template_second_instance():
    type_name = new_gtype_name()

    xml = f"""\
<interface>
  <template class="{type_name}" parent="GtkBox">
    <child>
      <object class="GtkLabel" id="label">
      </object>
    </child>
  </template>
</interface>
"""

    @Gtk.Template.from_string(xml)
    class Foo(Gtk.Box):
        __gtype_name__ = type_name

        label = Gtk.Template.Child("label")

        def __init__(self):
            super().__init__()
            self.init_template()

    # Stop current pygobject from handling the initialisation
    del Foo.__dontuse_ginstance_init__

    foo = Foo()
    assert isinstance(foo.label, Gtk.Label)

    foo2 = Foo()
    assert isinstance(foo2.label, Gtk.Label)


def test_main_example():
    type_name = new_gtype_name()

    example_xml = f"""\
<interface>
  <template class="{type_name}" parent="GtkBox">
    <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
    <property name="spacing">4</property>
    <child>
      <object class="GtkButton" id="hello_button">
        <property name="label">Hello World</property>
        <signal name="clicked" handler="hello_button_clicked"
                object="{type_name}" swapped="no"/>
        <signal name="clicked" handler="hello_button_clicked_after"
                object="{type_name}" swapped="no" after="yes"/>
      </object>
    </child>
    <child>
      <object class="GtkButton" id="goodbye_button">
        <property name="label">Goodbye World</property>
        <signal name="clicked" handler="goodbye_button_clicked"/>
        <signal name="clicked" handler="goodbye_button_clicked_after"
                after="yes"/>
      </object>
    </child>
  </template>
</interface>
"""

    @Gtk.Template.from_string(example_xml)
    class Foo(Gtk.Box):
        __gtype_name__ = type_name

        def __init__(self):
            super().__init__()
            self.callback_hello = []
            self.callback_hello_after = []
            self.callback_goodbye = []
            self.callback_goodbye_after = []

        @Gtk.Template.Callback("hello_button_clicked")
        def _hello_button_clicked(self, *args):
            self.callback_hello.append(args)

        @Gtk.Template.Callback("hello_button_clicked_after")
        def _hello_after(self, *args):
            self.callback_hello_after.append(args)

        _hello_button = Gtk.Template.Child("hello_button")

        goodbye_button = Gtk.Template.Child()

        @Gtk.Template.Callback("goodbye_button_clicked")
        def _goodbye_button_clicked(self, *args):
            self.callback_goodbye.append(args)

        @Gtk.Template.Callback("goodbye_button_clicked_after")
        def _goodbye_after(self, *args):
            self.callback_goodbye_after.append(args)

    w = Foo()
    assert w.__gtype__.name == type_name
    assert w.props.orientation == Gtk.Orientation.HORIZONTAL
    assert w.props.spacing == 4
    assert isinstance(w._hello_button, Gtk.Button)
    assert w._hello_button.props.label == "Hello World"
    assert isinstance(w.goodbye_button, Gtk.Button)
    assert w.goodbye_button.props.label == "Goodbye World"

    assert w.callback_hello == []
    w._hello_button.emit("clicked")
    assert w.callback_hello == [(w,)]
    assert w.callback_hello_after == [(w,)]

    assert w.callback_goodbye == []
    w.goodbye_button.emit("clicked")
    assert w.callback_goodbye == [(w.goodbye_button,)]
    assert w.callback_goodbye_after == [(w.goodbye_button,)]


def test_duplicate_handler():
    type_name = new_gtype_name()

    xml = f"""\
<interface>
  <template class="{type_name}" parent="GtkBox">
    <child>
      <object class="GtkButton" id="hello_button">
        <signal name="clicked" handler="hello_button_clicked" />
      </object>
    </child>
  </template>
</interface>
"""

    class Foo(Gtk.Box):
        __gtype_name__ = type_name

        @Gtk.Template.Callback("hello_button_clicked")
        def _hello_button_clicked(self, *args):
            pass

        @Gtk.Template.Callback()
        def hello_button_clicked(self, *args):
            pass

    with pytest.raises(RuntimeError, match=r".*hello_button_clicked.*"):
        Gtk.Template.from_string(xml)(Foo)


def test_duplicate_child():
    type_name = new_gtype_name()

    xml = f"""\
<interface>
  <template class="{type_name}" parent="GtkBox">
    <child>
      <object class="GtkButton" id="hello_button" />
    </child>
  </template>
</interface>
"""

    class Foo(Gtk.Box):
        __gtype_name__ = type_name

        foo = Gtk.Template.Child("hello_button")
        hello_button = Gtk.Template.Child()

    with pytest.raises(RuntimeError, match=r".*hello_button.*"):
        Gtk.Template.from_string(xml)(Foo)


def test_nonexist_handler():
    type_name = new_gtype_name()

    xml = f"""\
<interface>
  <template class="{type_name}" parent="GtkBox">
  </template>
</interface>
"""

    @Gtk.Template.from_string(xml)
    class Foo(Gtk.Box):
        __gtype_name__ = type_name

        @Gtk.Template.Callback("nonexit")
        def foo(self, *args):
            pass

    with capture_exceptions() as exc_info:
        Foo()
    assert "nonexit" in str(exc_info[0].value)
    assert exc_info[0].type is RuntimeError


def test_missing_handler_callback():
    type_name = new_gtype_name()

    xml = f"""\
<interface>
  <template class="{type_name}" parent="GtkBox">
    <child>
      <object class="GtkButton" id="hello_button">
        <signal name="clicked" handler="i_am_not_used_in_python" />
      </object>
    </child>
  </template>
</interface>
"""

    class Foo(Gtk.Box):
        __gtype_name__ = type_name

    Gtk.Template.from_string(xml)(Foo)()


def test_handler_swapped_not_supported():
    type_name = new_gtype_name()

    xml = f"""\
<interface>
  <template class="{type_name}" parent="GtkBox">
    <child>
      <object class="GtkButton" id="hello_button">
        <signal name="clicked" handler="hello_button_clicked"
                object="{type_name}" swapped="yes" />
      </object>
    </child>
  </template>
</interface>
"""

    @Gtk.Template.from_string(xml)
    class Foo(Gtk.Box):
        __gtype_name__ = type_name

        hello_button = Gtk.Template.Child()

        @Gtk.Template.Callback("hello_button_clicked")
        def foo(self, *args):
            pass

    with capture_exceptions() as exc_info:
        Foo()
    assert "ConnectFlags.SWAPPED" in str(exc_info[0].value)


def test_handler_class_staticmethod():
    type_name = new_gtype_name()

    xml = f"""\
<interface>
  <template class="{type_name}" parent="GtkBox">
    <child>
      <object class="GtkButton" id="hello_button">
        <signal name="clicked" handler="clicked_class" />
        <signal name="clicked" handler="clicked_static" />
      </object>
    </child>
  </template>
</interface>
"""

    signal_args_class = []
    signal_args_static = []

    @Gtk.Template.from_string(xml)
    class Foo(Gtk.Box):
        __gtype_name__ = type_name

        hello_button = Gtk.Template.Child()

        @Gtk.Template.Callback("clicked_class")
        @classmethod
        def cb1(*args):
            signal_args_class.append(args)

        @Gtk.Template.Callback("clicked_static")
        @staticmethod
        def cb2(*args):
            signal_args_static.append(args)

    foo = Foo()
    foo.hello_button.emit("clicked")
    assert signal_args_class == [(Foo, foo.hello_button)]
    assert signal_args_static == [(foo.hello_button,)]


@pytest.mark.skipif(Gtk._version == "4.0", reason="errors out first with gtk4")
def test_check_decorated_class():
    NonWidget = type("Foo", (object,), {})
    with pytest.raises(TypeError, match=r".*on Widgets.*"):
        Gtk.Template.from_string("")(NonWidget)

    Widget = type("Foo", (Gtk.Widget,), {"__gtype_name__": new_gtype_name()})
    with pytest.raises(TypeError, match=r".*Cannot nest.*"):
        Gtk.Template.from_string("")(Gtk.Template.from_string("")(Widget))

    Widget = type("Foo", (Gtk.Widget,), {})
    with pytest.raises(TypeError, match=r".*__gtype_name__.*"):
        Gtk.Template.from_string("")(Widget)

    with pytest.raises(TypeError, match=r".*on Widgets.*"):
        Gtk.Template.from_string("")(object())


@pytest.mark.skipif(Gtk._version == "4.0", reason="errors out first with gtk4")
def test_subclass_fail():
    @Gtk.Template.from_string("")
    class Base(Gtk.Widget):
        __gtype_name__ = new_gtype_name()

    with capture_exceptions() as exc_info:
        type("Sub", (Base,), {})()
    assert "not allowed at this time" in str(exc_info[0].value)
    assert exc_info[0].type is TypeError


def test_from_file():
    fd, name = tempfile.mkstemp()
    try:
        os.close(fd)

        type_name = new_gtype_name()

        with open(name, "wb") as h:
            h.write(
                f"""\
    <interface>
      <template class="{type_name}" parent="GtkBox">
      <property name="spacing">42</property>
      </template>
    </interface>
    """.encode()
            )

        @Gtk.Template.from_file(name)
        class Foo(Gtk.Box):
            __gtype_name__ = type_name

        foo = Foo()
        assert foo.props.spacing == 42
    finally:
        os.remove(name)


def test_property_override():
    type_name = new_gtype_name()

    xml = f"""\
    <interface>
      <template class="{type_name}" parent="GtkBox">
      <property name="spacing">42</property>
      </template>
    </interface>
"""

    @Gtk.Template.from_string(xml)
    class Foo(Gtk.Box):
        __gtype_name__ = type_name

    foo = Foo()
    assert foo.props.spacing == 42

    foo = Foo(spacing=124)
    assert foo.props.spacing == 124


def test_from_file_non_exist():
    dirname = tempfile.mkdtemp()
    try:
        path = os.path.join(dirname, "noexist")

        Widget = type("Foo", (Gtk.Widget,), {"__gtype_name__": new_gtype_name()})
        with pytest.raises(GLib.Error, match=r".*No such file.*"):
            Gtk.Template.from_file(path)(Widget)
    finally:
        os.rmdir(dirname)


def test_from_string_bytes():
    type_name = new_gtype_name()

    xml = f"""\
    <interface>
      <template class="{type_name}" parent="GtkBox">
      <property name="spacing">42</property>
      </template>
    </interface>
    """.encode()

    @Gtk.Template.from_string(xml)
    class Foo(Gtk.Box):
        __gtype_name__ = type_name

    foo = Foo()
    assert foo.props.spacing == 42


def test_from_resource():
    resource_path = ensure_resource_registered()

    @Gtk.Template.from_resource(resource_path)
    class Foo(Gtk.Box):
        __gtype_name__ = "GtkTemplateTestResource"

    foo = Foo()
    assert foo.props.spacing == 42


def test_from_resource_non_exit():
    Widget = type("Foo", (Gtk.Widget,), {"__gtype_name__": new_gtype_name()})
    with pytest.raises(GLib.Error, match=r".*/or/gnome/pygobject/noexit.*"):
        Gtk.Template.from_resource("/or/gnome/pygobject/noexit")(Widget)


def test_constructors():
    with pytest.raises(TypeError):
        Gtk.Template()

    with pytest.raises(TypeError):
        Gtk.Template(foo=1)

    Gtk.Template(filename="foo")
    Gtk.Template(resource_path="foo")
    Gtk.Template(string="foo")

    with pytest.raises(TypeError):
        Gtk.Template(filename="foo", resource_path="bar")

    with pytest.raises(TypeError):
        Gtk.Template(filename="foo", nope="bar")

    Gtk.Template.from_string("bla")
    Gtk.Template.from_resource("foo")
    Gtk.Template.from_file("foo")


def test_child_construct():
    Gtk.Template.Child()
    Gtk.Template.Child("name")
    with pytest.raises(TypeError):
        Gtk.Template.Child("name", True)
    Gtk.Template.Child("name", internal=True)
    with pytest.raises(TypeError):
        Gtk.Template.Child("name", internal=True, something=False)


def test_internal_child():
    main_type_name = new_gtype_name()

    xml = f"""\
    <interface>
      <template class="{main_type_name}" parent="GtkBox">
        <child>
          <object class="GtkBox" id="somechild">
            <property name="margin-top">42</property>
          </object>
        </child>
      </template>
    </interface>
    """

    @Gtk.Template.from_string(xml)
    class MainThing(Gtk.Box):
        __gtype_name__ = main_type_name

        somechild = Gtk.Template.Child(internal=True)

    thing = MainThing()
    assert thing.somechild.props.margin_top == 42

    other_type_name = new_gtype_name()

    xml = f"""\
    <interface>
      <template class="{other_type_name}" parent="GtkBox">
        <child>
          <object class="{main_type_name}">
            <child internal-child="somechild">
              <object class="GtkBox">
                <property name="margin-top">24</property>
                <child>
                  <object class="GtkLabel">
                    <property name="label">foo</property>
                  </object>
                </child>
              </object>
            </child>
          </object>
        </child>
      </template>
    </interface>
    """

    @Gtk.Template.from_string(xml)
    class OtherThing(Gtk.Box):
        __gtype_name__ = other_type_name

    other = OtherThing()
    child = other.get_children()[0] if not GTK4 else other.get_first_child()

    assert isinstance(child, MainThing)

    child = child.get_children()[0] if not GTK4 else child.get_first_child()

    assert isinstance(child, Gtk.Box)
    assert child.props.margin_top == 24

    child = child.get_children()[0] if not GTK4 else child.get_first_child()

    assert isinstance(child, Gtk.Label)
    assert child.props.label == "foo"


def test_template_hierarchy():
    testlabel = """
    <interface>
      <template class="TestLabel" parent="GtkLabel">
      </template>
     </interface>
    """

    @Gtk.Template(string=testlabel)
    class TestLabel(Gtk.Label):
        __gtype_name__ = "TestLabel"

        def __init__(self):
            super().__init__()
            self.props.label = "TestLabel"

    testbox = """
    <interface>
      <template class="TestBox" parent="GtkBox">
        <child>
          <object class="TestLabel" id="_testlabel"/>
        </child>
      </template>
    </interface>
    """

    @Gtk.Template(string=testbox)
    class TestBox(Gtk.Box):
        __gtype_name__ = "TestBox"

        _testlabel = Gtk.Template.Child()

        def __init__(self):
            super().__init__()

            assert isinstance(self._testlabel, TestLabel)

    window = """
    <interface>
      <template class="MyWindow" parent="GtkWindow">
        <property name="title">"Hellow World"</property>
        <child>
          <object class="TestBox" id="_testbox">
            <child>
              <object class="TestLabel" id="_testlabel"/>
            </child>
          </object>
        </child>
      </template>
    </interface>
    """

    @Gtk.Template(string=window)
    class MyWindow(Gtk.Window):
        __gtype_name__ = "MyWindow"

        _testbox = Gtk.Template.Child()
        _testlabel = Gtk.Template.Child()

        def __init__(self):
            super().__init__()

            assert isinstance(self._testbox, TestBox)
            assert isinstance(self._testlabel, TestLabel)
            children = self._testbox.get_children() if not GTK4 else list(self._testbox)

            assert len(children) == 2

    win = MyWindow()
    assert isinstance(win, MyWindow)


def test_multiple_init_template_calls():
    xml = """
    <interface>
      <template class="MyBox" parent="GtkBox">
        <child>
          <object class="GtkLabel" id="_label"/>
        </child>
       </template>
     </interface>
    """

    @Gtk.Template(string=xml)
    class MyBox(Gtk.Box):
        __gtype_name__ = "MyBox"

        _label = Gtk.Template.Child()

        def __init__(self):
            super().__init__()
            self._label.props.label = "awesome label"

    my_box = MyBox()
    assert isinstance(my_box, MyBox)
    children = my_box.get_children() if not GTK4 else list(my_box)

    assert len(children) == 1
    my_box.init_template()
    assert isinstance(my_box, MyBox)
    children = my_box.get_children() if not GTK4 else list(my_box)

    assert len(children) == 1


def test_python_class_hierarchy():
    xml = """
    <interface>
      <template class="ChildBox" parent="ParentBox">
        <child>
          <object class="GtkLabel" id="_label"/>
        </child>
      </template>
    </interface>
    """

    class ParentBox(Gtk.Box):
        __gtype_name__ = "ParentBox"

        def __init__(self):
            super().__init__()

    @Gtk.Template(string=xml)
    class ChildBox(ParentBox):
        __gtype_name__ = "ChildBox"

        _label = Gtk.Template.Child()

        def __init__(self):
            super().__init__()

    childBox = ChildBox()
    assert isinstance(childBox, ChildBox)
    children = childBox.get_children() if not GTK4 else list(childBox)

    assert len(children) == 1


@pytest.mark.skipif(not GTK4, reason="GTK 4 only")
def test_finalization_of_custom_child_objects():
    xml = """
    <?xml version="1.0" encoding="UTF-8"?>
    <interface>
    <requires lib="gtk" version="4.0" />
    <template class="CustomBox" parent="GtkBox">
        <child>
            <object class="CustomLabel">
                <property name="label">CustomLabel</property>
            </object>
        </child>
    </template>
    </interface>
    """

    class CustomLabel(Gtk.Label):
        __gtype_name__ = "CustomLabel"

        def do_unroot(self) -> None:
            check_finalize(self)

    @Gtk.Template(string=xml)
    class CustomBox(Gtk.Box):
        __gtype_name__ = "CustomBox"

        def do_unroot(self) -> None:
            check_finalize(self)

    finalized = []

    def check_finalize(obj) -> None:
        name = obj.__class__.__name__
        obj.weak_ref(lambda: finalized.append(f"{name} -> GObject finalized"))
        weakref.finalize(obj, finalized.append, f"{name} -> PyObject finalized")

    custom_box = CustomBox()
    box = Gtk.Box()
    box.append(custom_box)
    window = Gtk.Window()
    window.set_child(box)
    window.present()
    box.remove(custom_box)

    del custom_box

    assert "CustomBox -> PyObject finalized" in finalized
    assert "CustomBox -> GObject finalized" in finalized
    assert "CustomLabel -> PyObject finalized" in finalized
    assert "CustomLabel -> GObject finalized" in finalized


def test_signal_handler_with_object_does_not_leak_memory():
    """Regression test for a memory leak when using signal handlers in GTK templates.
    Note that this example is intentionally minimal, i.e. the handler doesn't even
    need to exist on the class. The only requirement is that the `object` is specified.
    """
    type_name = new_gtype_name()
    xml = f"""\
    <?xml version="1.0" encoding="UTF-8"?>
    <interface>
    <requires lib="gtk" version="4.0" />
    <template class="{type_name}" parent="GtkWidget">
        <signal name="show" handler="on_show" object="{type_name}" swapped="no" />
    </template>
    </interface>
    """

    @Gtk.Template(string=xml)
    class MyWidget(Gtk.Widget):
        __gtype_name__ = type_name

    weak = MyWidget().weak_ref()

    gc.collect()
    gc.collect()  # One more for PyPy

    assert weak() is None
