import asyncio
import copy
import unittest
import xmlrpc.client
from typing import Tuple, Optional

import aioresponses

from hippolyzer.lib.base import llsd
from hippolyzer.lib.base.datatypes import UUID
from hippolyzer.lib.base.message.circuit import Circuit
from hippolyzer.lib.base.message.message import Message, Block
from hippolyzer.lib.base.message.message_handler import MessageHandler
from hippolyzer.lib.base.message.msgtypes import PacketFlags
from hippolyzer.lib.base.message.udpdeserializer import UDPMessageDeserializer
from hippolyzer.lib.base.network.transport import AbstractUDPTransport, UDPPacket, Direction
from hippolyzer.lib.base.test_utils import MockTransport, MockConnectionHolder, soon
from hippolyzer.lib.client.hippo_client import HippoClient, HippoClientProtocol


class MockServer(MockConnectionHolder):
    def __init__(self, circuit, message_handler):
        super().__init__(circuit, message_handler)
        self.deserializer = UDPMessageDeserializer()
        self.protocol: Optional[HippoClientProtocol] = None

    def process_inbound(self, packet: UDPPacket):
        """Process a packet that the client sent to us"""
        message = self.deserializer.deserialize(packet.data)
        message.direction = Direction.IN
        if message.reliable:
            self.circuit.send_acks((message.packet_id,))
        self.circuit.collect_acks(message)
        if message.name != "PacketAck":
            self.message_handler.handle(message)


class PacketForwardingTransport(MockTransport):
    def __init__(self):
        super().__init__()
        self.protocol: Optional[HippoClientProtocol] = None

    def send_packet(self, packet: UDPPacket):
        super().send_packet(packet)
        self.protocol.datagram_received(packet.data, packet.src_addr)


class MockServerTransport(MockTransport):
    """Used for the client to send packets out"""
    def __init__(self, server: MockServer):
        super().__init__()
        self._server = server

    def send_packet(self, packet: UDPPacket) -> None:
        super().send_packet(packet)
        # Directly pass the packet to the server
        packet = copy.copy(packet)
        packet.direction = Direction.IN
        # Delay calling so the client can do its ACK bookkeeping first
        asyncio.get_event_loop().call_soon(lambda: self._server.process_inbound(packet))


class MockHippoClient(HippoClient):
    def __init__(self, server: MockServer):
        super().__init__()
        self.server = server

    async def _create_transport(self) -> Tuple[AbstractUDPTransport, HippoClientProtocol]:
        protocol = HippoClientProtocol(self.session)
        # TODO: This isn't great, but whatever.
        self.server.circuit.transport.protocol = protocol
        return MockServerTransport(self.server), protocol


class TestHippoClient(unittest.IsolatedAsyncioTestCase):
    FAKE_LOGIN_URI = "http://127.0.0.1:1/login.cgi"
    FAKE_LOGIN_RESP = {
        "session_id": str(UUID(int=1)),
        "secure_session_id": str(UUID(int=2)),
        "agent_id": str(UUID(int=3)),
        "circuit_code": 123,
        "sim_ip": "127.0.0.1",
        "sim_port": 2,
        "region_x": 0,
        "region_y": 123,
        "seed_capability": "https://127.0.0.1:4/foo",
        "inventory-skeleton": [
            {'name': 'My Inventory', 'folder_id': str(UUID(int=4)),
             'parent_id': '00000000-0000-0000-0000-000000000000', 'type_default': 8, 'version': 200}
        ]
    }
    FAKE_SEED_RESP = {
        "EventQueueGet": "https://127.0.0.1:5/",
    }
    FAKE_EQ_RESP = {
        "id": 1,
        "events": [
            {"message": "ViewerFrozenMessage", "body": {"FrozenData": [{"Data": False}]}},
            {"message": "NotTemplated", "body": {"foo": {"bar": True}}},
        ],
    }

    async def asyncSetUp(self):
        self.server_handler: MessageHandler[Message, str] = MessageHandler()
        self.server_transport = PacketForwardingTransport()
        self.server_circuit = Circuit(("127.0.0.1", 2), ("127.0.0.1", 99), self.server_transport)
        self.server = MockServer(self.server_circuit, self.server_handler)
        self.aio_mock = aioresponses.aioresponses()
        self.aio_mock.start()
        self.aio_mock.post(
            self.FAKE_LOGIN_URI,
            body=xmlrpc.client.dumps((self.FAKE_LOGIN_RESP,), None, True)
        )
        self.aio_mock.post(self.FAKE_LOGIN_RESP['seed_capability'], body=llsd.format_xml(self.FAKE_SEED_RESP))
        self.aio_mock.post(self.FAKE_SEED_RESP['EventQueueGet'], body=llsd.format_xml(self.FAKE_EQ_RESP), repeat=True)
        self.client = MockHippoClient(self.server)

    async def asyncTearDown(self):
        try:
            await self.client.aclose()
        finally:
            self.aio_mock.stop()

    async def _log_client_in(self, client: MockHippoClient):
        login_task = asyncio.create_task(client.login("foo", "bar", login_uri=self.FAKE_LOGIN_URI))
        with self.server_handler.subscribe_async(
                ("*",),
        ) as get_msg:
            assert (await soon(get_msg())).name == "UseCircuitCode"
            assert (await soon(get_msg())).name == "CompleteAgentMovement"
            self.server.circuit.send(Message(
                'RegionHandshake',
                Block('RegionInfo', fill_missing=True),
                Block('RegionInfo2', fill_missing=True),
                Block('RegionInfo3', fill_missing=True),
                Block('RegionInfo4', fill_missing=True),
            ))
            assert (await soon(get_msg())).name == "RegionHandshakeReply"
            assert (await soon(get_msg())).name == "AgentThrottle"
            await login_task

    async def test_login(self):
        await self._log_client_in(self.client)
        with self.server_handler.subscribe_async(
                ("*",),
        ) as get_msg:
            self.client.logout()
            assert (await soon(get_msg())).name == "LogoutRequest"

    async def test_eq(self):
        await self._log_client_in(self.client)
        with self.client.session.message_handler.subscribe_async(
                ("ViewerFrozenMessage", "NotTemplated"),
        ) as get_msg:
            assert (await soon(get_msg())).name == "ViewerFrozenMessage"
            msg = await soon(get_msg())
            assert msg.name == "NotTemplated"
            assert msg["EventData"]["foo"]["bar"] == 1

    async def test_inventory_manager(self):
        await self._log_client_in(self.client)
        self.assertEqual(self.client.session.inventory.model.root.node_id, UUID(int=4))

    async def test_resend_suppression(self):
        """Make sure the client only handles the first seen copy of a reliable message"""
        await self._log_client_in(self.client)
        with self.client.session.message_handler.subscribe_async(
                ("ChatFromSimulator", "AgentDataUpdate"),
        ) as get_msg:
            msg = Message("ChatFromSimulator", Block("ChatData", fill_missing=True))
            msg.send_flags |= PacketFlags.RELIABLE

            # Fake re-sending the message
            packet = self.server_circuit.send(msg)
            self.server_transport.send_packet(packet)

            self.server_circuit.send(Message("AgentDataUpdate", Block("AgentData", fill_missing=True)))
            assert (await soon(get_msg())).name == "ChatFromSimulator"
            assert (await soon(get_msg())).name == "AgentDataUpdate"
