import tempfile
import os
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
