"""This tests the ability to send and receive file descriptors in dbus messages"""
from asyncdbus.service import ServiceInterface, method, signal, dbus_property
from asyncdbus.signature import SignatureTree, Variant, UnixFD, Str, Array, Var, Struct, Dict, Tuple
from asyncdbus import Message, MessageType, MessageBus
import os
import anyio
import pytest


def open_file():
    return os.open(os.devnull, os.O_RDONLY)


class ExampleInterface(ServiceInterface):
    def __init__(self, name):
        super().__init__(name)
        self.fds = []

    @method()
    def ReturnsFd(self) -> UnixFD:
        fd = open_file()
        self.fds.append(fd)
        return fd

    @method()
    def AcceptsFd(self, fd: UnixFD):
        assert fd != 0
        self.fds.append(fd)

    def get_last_fd(self):
        return self.fds[-1]

    def cleanup(self):
        for fd in self.fds:
            os.close(fd)
        self.fds.clear()

    @signal()
    def SignalFd(self) -> UnixFD:
        fd = open_file()
        self.fds.append(fd)
        return fd

    @dbus_property()
    def PropFd(self) -> UnixFD:
        if not self.fds:
            fd = open_file()
            self.fds.append(fd)
        return self.fds[-1]

    @PropFd.setter
    def PropFd(self, fd: UnixFD):
        assert fd
        self.fds.append(fd)


def assert_fds_equal(fd1, fd2):
    assert fd1
    assert fd2

    stat1 = os.fstat(fd1)
    stat2 = os.fstat(fd2)

    assert stat1.st_dev == stat2.st_dev
    assert stat1.st_ino == stat2.st_ino
    assert stat1.st_rdev == stat2.st_rdev


@pytest.mark.anyio
async def test_sending_file_descriptor_low_level():
    async with MessageBus(negotiate_unix_fd=True).connect() as bus1, \
            MessageBus(negotiate_unix_fd=True).connect() as bus2:

        fd_before = open_file()
        fd_after = None

        msg = Message(
            destination=bus1.unique_name,
            path='/org/test/path',
            interface='org.test.iface',
            member='SomeMember',
            body=[0],
            signature=UnixFD,
            unix_fds=[fd_before])

        async def message_handler(sent):
            nonlocal fd_after
            if sent.sender == bus2.unique_name and sent.serial == msg.serial:
                assert sent.path == msg.path
                assert sent.serial == msg.serial
                assert sent.interface == msg.interface
                assert sent.member == msg.member
                assert sent.body == [0]
                assert len(sent.unix_fds) == 1
                fd_after = sent.unix_fds[0]
                await bus1.send(Message.new_method_return(sent, Str, ['got it']))
                bus1.remove_message_handler(message_handler)
                return True

        bus1.add_message_handler(message_handler)

        reply = await bus2.call(msg)
        assert reply.body == ['got it']
        assert fd_after is not None

        assert_fds_equal(fd_before, fd_after)

        for fd in [fd_before, fd_after]:
            os.close(fd)


@pytest.mark.anyio
async def test_high_level_service_fd_passing():
    async with MessageBus(negotiate_unix_fd=True).connect() as bus1, \
            MessageBus(negotiate_unix_fd=True).connect() as bus2:

        interface_name = 'test.interface'
        interface = ExampleInterface(interface_name)
        export_path = '/test/path'

        async def call(member, signature='', body=[], unix_fds=[], iface=interface.name):
            return await bus2.call(
                Message(
                    destination=bus1.unique_name,
                    path=export_path,
                    interface=iface,
                    member=member,
                    signature=signature,
                    body=body,
                    unix_fds=unix_fds))

        await bus1.export(export_path, interface)

        # test that an fd can be returned by the service
        reply = await call('ReturnsFd')
        assert reply.message_type == MessageType.METHOD_RETURN, reply.body
        assert reply.signature == UnixFD.tree.signature
        assert len(reply.unix_fds) == 1
        assert_fds_equal(interface.get_last_fd(), reply.unix_fds[0])
        interface.cleanup()
        os.close(reply.unix_fds[0])

        # test that an fd can be sent to the service
        fd = open_file()
        reply = await call('AcceptsFd', signature=UnixFD, body=[0], unix_fds=[fd])
        assert reply.message_type == MessageType.METHOD_RETURN, reply.body
        assert_fds_equal(interface.get_last_fd(), fd)

        interface.cleanup()
        os.close(fd)

        # signals
        evt = anyio.Event()
        res = None

        def fd_listener(msg):
            nonlocal res
            if msg.sender == bus1.unique_name and msg.message_type == MessageType.SIGNAL:
                res = msg
                evt.set()

        reply = await bus2.call(
            Message(
                destination='org.freedesktop.DBus',
                path='/org/freedesktop/DBus',
                member='AddMatch',
                signature=Str,
                body=[f"sender='{bus1.unique_name}'"]))
        assert reply.message_type == MessageType.METHOD_RETURN

        bus2.add_message_handler(fd_listener)
        await interface.SignalFd()
        await evt.wait()
        reply = res

        assert len(reply.unix_fds) == 1, (reply.unix_fds, interface.get_last_fd())
        assert reply.body == [0]
        assert_fds_equal(reply.unix_fds[0], interface.get_last_fd())

        interface.cleanup()
        os.close(reply.unix_fds[0])

        # properties
        reply = await call(
            'Get',
            Tuple[Str, Str], [interface_name, 'PropFd'],
            iface='org.freedesktop.DBus.Properties')
        assert reply.message_type == MessageType.METHOD_RETURN, reply.body
        assert reply.body[0].signature == UnixFD.tree.signature
        assert reply.body[0].value == 0
        assert len(reply.unix_fds) == 1
        assert_fds_equal(interface.get_last_fd(), reply.unix_fds[0])
        interface.cleanup()
        os.close(reply.unix_fds[0])

        fd = open_file()
        reply = await call(
            'Set',
            Tuple[Str, Str, Var],
            [interface_name, 'PropFd', Variant(UnixFD, 0)],
            iface='org.freedesktop.DBus.Properties',
            unix_fds=[fd])
        assert reply.message_type == MessageType.METHOD_RETURN, reply.body
        assert_fds_equal(interface.get_last_fd(), fd)
        interface.cleanup()
        os.close(fd)

        reply = await call('GetAll', Str, [interface_name], iface='org.freedesktop.DBus.Properties')
        assert reply.message_type == MessageType.METHOD_RETURN, reply.body
        assert reply.body[0]['PropFd'].signature == UnixFD.tree.signature
        assert reply.body[0]['PropFd'].value == 0
        assert len(reply.unix_fds) == 1
        assert_fds_equal(interface.get_last_fd(), reply.unix_fds[0])
        interface.cleanup()
        os.close(reply.unix_fds[0])


@pytest.mark.anyio
async def test_sending_file_descriptor_with_proxy():
    name = 'dbus.next.test.service'
    path = '/test/path'
    interface_name = 'test.interface'

    async with MessageBus(negotiate_unix_fd=True).connect() as bus:

        interface = ExampleInterface(interface_name)
        await bus.export(path, interface)
        await bus.request_name(name)

        intr = await bus.introspect(name, path)

        proxy = await bus.get_proxy_object(name, path, intr)
        proxy_interface = await proxy.get_interface(interface_name)

        # test fds are replaced correctly in all high level interfaces
        fd = await proxy_interface.call_returns_fd()
        assert_fds_equal(interface.get_last_fd(), fd)
        interface.cleanup()
        os.close(fd)

        fd = open_file()
        await proxy_interface.call_accepts_fd(fd)
        assert_fds_equal(interface.get_last_fd(), fd)
        interface.cleanup()
        os.close(fd)

        fd = await proxy_interface.get_prop_fd()
        assert_fds_equal(interface.get_last_fd(), fd)
        interface.cleanup()
        os.close(fd)

        fd = open_file()
        await proxy_interface.set_prop_fd(fd)
        assert_fds_equal(interface.get_last_fd(), fd)
        interface.cleanup()
        os.close(fd)

        evt = anyio.Event()
        res = None

        async def on_signal_fd(fd):
            nonlocal res
            res = fd
            evt.set()
            await proxy_interface.off_signal_fd(on_signal_fd)

        await proxy_interface.on_signal_fd(on_signal_fd)
        await anyio.sleep(0.1)  # TODO we have a timing problem here
        await interface.SignalFd()
        await evt.wait()
        fd = res
        assert_fds_equal(interface.get_last_fd(), fd)
        interface.cleanup()
        os.close(fd)
        pass  # closing


@pytest.mark.anyio
@pytest.mark.parametrize(
    "result, out_signature, expected",
    [
        pytest.param(5, UnixFD, ([0], [5]), id='Signature: "h"'),
        pytest.param([5, "foo"], 'hs', ([0, "foo"], [5]), id='Signature: "hs"'),
        pytest.param([5, 7], 'hh', ([0, 1], [5, 7]), id='Signature: "hh"'),
        pytest.param([5, 7], 'ah', ([[0, 1]], [5, 7]), id='Signature: "ah"'),
        pytest.param([9], Array[UnixFD], ([[0]], [9]), id='Signature: "ah"'),
        pytest.param([3], Struct[UnixFD], ([[0]], [3]), id='Signature: "(h)"'),
        pytest.param([3, "foo"], Struct[UnixFD, Str], ([[0, "foo"]], [3]), id='Signature: "(hs)"'),
        pytest.param([[7, "foo"], [8, "bar"]],
                     Array[Struct[UnixFD, Str]], ([[[0, "foo"], [1, "bar"]]], [7, 8]),
                     id='Signature: "a(hs)"'),
        pytest.param(
            {
                "foo": 3
            }, Array[Dict[Str, UnixFD]], ([{
                "foo": 0
            }], [3]), id='Signature: "a{sh}"'),
        pytest.param({
            "foo": 3,
            "bar": 6
        },
                     Array[Dict[Str, UnixFD]], ([{
                         "foo": 0,
                         "bar": 1
                     }], [3, 6]),
                     id='Signature: "a{sh}"'),
        pytest.param({
            "foo": [3, 8]
        },
                     Array[Dict[Str, Array[UnixFD]]], ([{
                         "foo": [0, 1]
                     }], [3, 8]),
                     id='Signature: "a{sah}"'),
        pytest.param({
            'foo': Variant('t', 100)
        },
                     Array[Dict[Str, Var]], ([{
                         'foo': Variant('t', 100)
                     }], []),
                     id='Signature: "a{sv}"'),
        pytest.param(['one', ['two', [Variant(Str, 'three')]]],
                     Struct[Str, Struct[Str, Struct[Var]]],
                     ([['one', ['two', [Variant(Str, 'three')]]]], []),
                     id='Signature: "(s(s(v)))"'),
        pytest.param(Variant(UnixFD, 2), 'v', ([Variant(UnixFD, 0)], [2]), id='Variant with: "h"'),
        pytest.param(
            Variant('(hh)', [2, 8]),
            'v', ([Variant('(hh)', [0, 1])], [2, 8]),
            id='Variant with: "(hh)"'),
        pytest.param(
            Variant('ah', [2, 4]), 'v', ([Variant('ah', [0, 1])], [2, 4]), id='Variant with: "ah"'),
        pytest.param(
            Variant('(ss)', ['hello', 'world']),
            'v', ([Variant('(ss)', ['hello', 'world'])], []),
            id='Variant with: "(ss)"'),
        pytest.param(
            Variant('v', Variant('t', 100)),
            'v', ([Variant('v', Variant('t', 100))], []),
            id='Variant with: "v"'),
        pytest.param(
            [
                Variant('v', Variant('(ss)', ['hello', 'world'])), {
                    'foo': Variant('t', 100)
                }, ['one', ['two', [Variant(Str, 'three')]]]
            ],
            'va{sv}(s(s(v)))', ([
                Variant('v', Variant('(ss)', ['hello', 'world'])), {
                    'foo': Variant('t', 100)
                }, ['one', ['two', [Variant(Str, 'three')]]]
            ], []),
            id='Variant with: "va{sv}(s(s(v)))"'),
    ],
)
async def test_fn_result_to_body(result, out_signature, expected):
    out_signature_tree = SignatureTree(out_signature)
    assert ServiceInterface._fn_result_to_body(result, out_signature_tree) == expected
