import pytest

from dbus_fast import Message
from dbus_fast.aio import MessageBus
from dbus_fast.constants import RequestNameReply
from dbus_fast.introspection import Node
from dbus_fast.service import ServiceInterface, signal
from dbus_fast.signature import Variant


class ExampleInterface(ServiceInterface):
    def __init__(self):
        super().__init__("test.interface")

    @signal()
    def SomeSignal(self) -> "s":
        return "hello"

    @signal()
    def SignalMultiple(self) -> "ss":
        return ["hello", "world"]

    @signal()
    def SignalComplex(self) -> "a{sv}":
        """Broadcast a complex signal."""
        return {"hello": Variant("s", "world")}


@pytest.mark.asyncio
async def test_signals():
    bus1 = await MessageBus().connect()
    bus2 = await MessageBus().connect()

    bus_intr = await bus1.introspect("org.freedesktop.DBus", "/org/freedesktop/DBus")
    bus_obj = bus1.get_proxy_object(
        "org.freedesktop.DBus", "/org/freedesktop/DBus", bus_intr
    )
    stats = bus_obj.get_interface("org.freedesktop.DBus.Debug.Stats")

    await bus1.request_name("test.signals.name")
    service_interface = ExampleInterface()
    bus1.export("/test/path", service_interface)

    obj = bus2.get_proxy_object(
        "test.signals.name", "/test/path", bus1._introspect_export_path("/test/path")
    )
    interface = obj.get_interface(service_interface.name)

    async def ping():
        await bus2.call(
            Message(
                destination=bus1.unique_name,
                interface="org.freedesktop.DBus.Peer",
                path="/test/path",
                member="Ping",
            )
        )

    err = None

    single_counter = 0

    def single_handler(value):
        try:
            nonlocal single_counter
            nonlocal err
            assert value == "hello"
            single_counter += 1
        except Exception as e:
            err = e

    multiple_counter = 0

    def multiple_handler(value1, value2):
        nonlocal multiple_counter
        nonlocal err
        try:
            assert value1 == "hello"
            assert value2 == "world"
            multiple_counter += 1
        except Exception as e:
            err = e

    await ping()
    match_rules = await stats.call_get_all_match_rules()
    assert bus2.unique_name in match_rules
    bus_match_rules = match_rules[bus2.unique_name]
    # the bus connection itself takes a rule on NameOwnerChange after the high
    # level client is initialized
    assert len(bus_match_rules) == 1
    assert len(bus2._user_message_handlers) == 0

    interface.on_some_signal(single_handler)
    interface.on_signal_multiple(multiple_handler)

    # Interlude: adding a signal handler with `on_[signal]` should add a match rule and
    # message handler. Removing a signal handler with `off_[signal]` should
    # remove the match rule and message handler to avoid memory leaks.
    await ping()
    match_rules = await stats.call_get_all_match_rules()
    assert bus2.unique_name in match_rules
    bus_match_rules = match_rules[bus2.unique_name]
    # test the match rule and user handler has been added
    assert len(bus_match_rules) == 2
    assert (
        "type='signal',interface='test.interface',path='/test/path',sender='test.signals.name'"
        in bus_match_rules
    )
    assert len(bus2._user_message_handlers) == 1

    service_interface.SomeSignal()
    await ping()
    assert err is None
    assert single_counter == 1

    service_interface.SignalMultiple()
    await ping()
    assert err is None
    assert multiple_counter == 1

    # special case: another bus with the same path and interface but on a
    # different name and connection will trigger the match rule of the first
    # (happens with mpris)
    bus3 = await MessageBus().connect()
    await bus3.request_name("test.signals.name2")
    service_interface2 = ExampleInterface()
    bus3.export("/test/path", service_interface2)

    obj = bus2.get_proxy_object(
        "test.signals.name2", "/test/path", bus3._introspect_export_path("/test/path")
    )
    # we have to add a dummy handler to add the match rule
    iface2 = obj.get_interface(service_interface2.name)

    def dummy_signal_handler(what):
        pass

    iface2.on_some_signal(dummy_signal_handler)
    await ping()

    service_interface2.SomeSignal()
    await ping()
    # single_counter is not incremented for signals of the second interface
    assert single_counter == 1

    interface.off_some_signal(single_handler)
    interface.off_signal_multiple(multiple_handler)
    iface2.off_some_signal(dummy_signal_handler)

    # After `off_[signal]`, the match rule and user handler should be removed
    await ping()
    match_rules = await stats.call_get_all_match_rules()
    assert bus2.unique_name in match_rules
    bus_match_rules = match_rules[bus2.unique_name]
    assert len(bus_match_rules) == 1
    assert (
        "type='signal',interface='test.interface',path='/test/path',sender='test.signals.name'"
        not in bus_match_rules
    )
    assert len(bus2._user_message_handlers) == 0

    bus1.disconnect()
    bus2.disconnect()
    bus3.disconnect()


@pytest.mark.asyncio
async def test_complex_signals():
    """Test complex signals with and without signature removal."""
    bus1 = await MessageBus().connect()
    bus2 = await MessageBus().connect()

    await bus1.request_name("test.signals.name")
    service_interface = ExampleInterface()
    bus1.export("/test/path", service_interface)

    obj = bus2.get_proxy_object(
        "test.signals.name", "/test/path", bus1._introspect_export_path("/test/path")
    )
    interface = obj.get_interface(service_interface.name)

    async def ping():
        await bus2.call(
            Message(
                destination=bus1.unique_name,
                interface="org.freedesktop.DBus.Peer",
                path="/test/path",
                member="Ping",
            )
        )

    sig_handler_counter = 0
    sig_handler_err = None
    no_sig_handler_counter = 0
    no_sig_handler_err = None

    def complex_handler_with_sig(value):
        nonlocal sig_handler_counter
        nonlocal sig_handler_err
        try:
            assert value == {"hello": Variant("s", "world")}
            sig_handler_counter += 1
        except AssertionError as ex:
            sig_handler_err = ex

    def complex_handler_no_sig(value):
        nonlocal no_sig_handler_counter
        nonlocal no_sig_handler_err
        try:
            assert value == {"hello": "world"}
            no_sig_handler_counter += 1
        except AssertionError as ex:
            no_sig_handler_err = ex

    interface.on_signal_complex(complex_handler_with_sig)
    interface.on_signal_complex(complex_handler_no_sig, unpack_variants=True)
    await ping()

    service_interface.SignalComplex()
    await ping()
    assert sig_handler_err is None
    assert sig_handler_counter == 1
    assert no_sig_handler_err is None
    assert no_sig_handler_counter == 1

    bus1.disconnect()
    bus2.disconnect()


@pytest.mark.asyncio
async def test_varargs_callback():
    """Test varargs callback for signal."""
    bus1 = await MessageBus().connect()
    bus2 = await MessageBus().connect()

    await bus1.request_name("test.signals.name")
    service_interface = ExampleInterface()
    bus1.export("/test/path", service_interface)

    obj = bus2.get_proxy_object(
        "test.signals.name", "/test/path", bus1._introspect_export_path("/test/path")
    )
    interface = obj.get_interface(service_interface.name)

    async def ping():
        await bus2.call(
            Message(
                destination=bus1.unique_name,
                interface="org.freedesktop.DBus.Peer",
                path="/test/path",
                member="Ping",
            )
        )

    varargs_handler_counter = 0
    varargs_handler_err = None
    varargs_plus_handler_counter = 0
    varargs_plus_handler_err = None

    def varargs_handler(*args):
        nonlocal varargs_handler_counter
        nonlocal varargs_handler_err
        try:
            assert args[0] == "hello"
            varargs_handler_counter += 1
        except AssertionError as ex:
            varargs_handler_err = ex

    def varargs_plus_handler(value, *_):
        nonlocal varargs_plus_handler_counter
        nonlocal varargs_plus_handler_err
        try:
            assert value == "hello"
            varargs_plus_handler_counter += 1
        except AssertionError as ex:
            varargs_plus_handler_err = ex

    interface.on_some_signal(varargs_handler)
    interface.on_some_signal(varargs_plus_handler)
    await ping()

    service_interface.SomeSignal()
    await ping()
    assert varargs_handler_err is None
    assert varargs_handler_counter == 1
    assert varargs_plus_handler_err is None
    assert varargs_plus_handler_counter == 1

    bus1.disconnect()
    bus2.disconnect()


@pytest.mark.asyncio
async def test_kwargs_callback():
    """Test callback for signal with kwargs."""
    bus1 = await MessageBus().connect()
    bus2 = await MessageBus().connect()

    await bus1.request_name("test.signals.name")
    service_interface = ExampleInterface()
    bus1.export("/test/path", service_interface)

    obj = bus2.get_proxy_object(
        "test.signals.name", "/test/path", bus1._introspect_export_path("/test/path")
    )
    interface = obj.get_interface(service_interface.name)

    async def ping():
        await bus2.call(
            Message(
                destination=bus1.unique_name,
                interface="org.freedesktop.DBus.Peer",
                path="/test/path",
                member="Ping",
            )
        )

    kwargs_handler_counter = 0
    kwargs_handler_err = None
    kwarg_default_handler_counter = 0
    kwarg_default_handler_err = None

    def kwargs_handler(value, **_):
        nonlocal kwargs_handler_counter
        nonlocal kwargs_handler_err
        try:
            assert value == "hello"
            kwargs_handler_counter += 1
        except AssertionError as ex:
            kwargs_handler_err = ex

    def kwarg_default_handler(value, *, _=True):
        nonlocal kwarg_default_handler_counter
        nonlocal kwarg_default_handler_err
        try:
            assert value == "hello"
            kwarg_default_handler_counter += 1
        except AssertionError as ex:
            kwarg_default_handler_err = ex

    interface.on_some_signal(kwargs_handler)
    interface.on_some_signal(kwarg_default_handler)
    await ping()

    service_interface.SomeSignal()
    await ping()
    assert kwargs_handler_err is None
    assert kwargs_handler_counter == 1
    assert kwarg_default_handler_err is None
    assert kwarg_default_handler_counter == 1

    def kwarg_bad_handler(value, *, bad_kwarg):
        pass

    with pytest.raises(TypeError):
        interface.on_some_signal(kwarg_bad_handler)

    bus1.disconnect()
    bus2.disconnect()


@pytest.mark.asyncio
async def test_on_signal_type_error():
    """Test on callback raises type errors for invalid callbacks."""
    bus1 = await MessageBus().connect()
    bus2 = await MessageBus().connect()

    await bus1.request_name("test.signals.name")
    service_interface = ExampleInterface()
    bus1.export("/test/path", service_interface)

    obj = bus2.get_proxy_object(
        "test.signals.name", "/test/path", bus1._introspect_export_path("/test/path")
    )
    interface = obj.get_interface(service_interface.name)

    with pytest.raises(TypeError):
        interface.on_some_signal("not_a_callable")

    with pytest.raises(TypeError):
        interface.on_some_signal(lambda a, b: "Too many parameters")

    with pytest.raises(TypeError):
        interface.on_some_signal(lambda: "Too few parameters")

    with pytest.raises(TypeError):
        interface.on_some_signal(lambda a, b, *args: "Too many before varargs")

    bus1.disconnect()
    bus2.disconnect()


@pytest.mark.asyncio
async def test_signals_with_changing_owners():
    well_known_name = "test.signals.changing.name"

    bus1 = await MessageBus().connect()
    bus2 = await MessageBus().connect()
    bus3 = await MessageBus().connect()

    async def ping():
        await bus1.call(
            Message(
                destination=bus1.unique_name,
                interface="org.freedesktop.DBus.Peer",
                path="/test/path",
                member="Ping",
            )
        )

    service_interface = ExampleInterface()
    introspection = Node.default()
    introspection.interfaces.append(service_interface.introspect())

    # get the interface before export
    obj = bus1.get_proxy_object(well_known_name, "/test/path", introspection)
    iface = obj.get_interface("test.interface")
    counter = 0

    def handler(what):
        nonlocal counter
        counter += 1

    iface.on_some_signal(handler)
    await ping()

    # now export and get the name
    bus2.export("/test/path", service_interface)
    result = await bus2.request_name(well_known_name)
    assert result is RequestNameReply.PRIMARY_OWNER

    # the signal should work
    service_interface.SomeSignal()
    await ping()
    assert counter == 1
    counter = 0

    # now queue up a transfer of the name
    service_interface2 = ExampleInterface()
    bus3.export("/test/path", service_interface2)
    result = await bus3.request_name(well_known_name)
    assert result is RequestNameReply.IN_QUEUE

    # if it doesn't own the name, the signal shouldn't work here
    service_interface2.SomeSignal()
    await ping()
    assert counter == 0

    # now transfer over the name and it should work
    bus2.disconnect()
    await ping()

    service_interface2.SomeSignal()
    await ping()
    assert counter == 1
    counter = 0

    bus1.disconnect()
    bus2.disconnect()
    bus3.disconnect()
