import asyncio

import pytest

from aiospb.infra.encoding import JsonPayloadEncoder, ProtobufPayloadEncoder
from aiospb.infra.inmem import InMemHistorian, InMemReadingStore
from aiospb.messages import Event, Message, MQTTMessage, NodeMessageSession
from aiospb.nodes import EdgeNode, MetricInfo
from aiospb.nodes.devices import NodeDevice
from aiospb.shared import (
    DataType,
    DeviceKey,
    Metric,
    NodeOptions,
    PropertySet,
    PropertyValue,
)
from tests.fakes import EasyMetricsNet, FakeMQTTClient, ts

NKEY = DeviceKey("group/node")
DKEY = DeviceKey("group/node/device")
HKEY = DeviceKey("hostname")


@pytest.fixture
def net():
    n = EasyMetricsNet(
        {
            NKEY: [
                MetricInfo(
                    "Node/Temperature",
                    DataType.Int16,
                    PropertySet({"engUnit": PropertyValue("dgradC", DataType.String)}),
                    1,
                )
            ],
            DKEY: [
                MetricInfo(
                    "Device/Temperature",
                    DataType.Float,
                    PropertySet({"engUnit": PropertyValue("gradC", DataType.String)}),
                    2,
                )
            ],
        }
    )
    return n


@pytest.fixture
def mqtt_client():
    return FakeMQTTClient()


@pytest.fixture
def node(mqtt_client, net):
    session = NodeMessageSession(
        NKEY, mqtt_client, ProtobufPayloadEncoder(), JsonPayloadEncoder(), NodeOptions()
    )
    readings = InMemReadingStore()
    n = EdgeNode(
        NodeDevice(NKEY, net, readings),
        session,
        InMemHistorian(),
        NodeOptions(scan_rate=50),  # Scan every 0.05 s
    )
    n.add_device(NodeDevice(DKEY, net, readings))
    return n


class A_Node:
    async def when_host_publish_state(self, mqtt_client, online: bool):
        msg = Message(
            Event.HOST_STATE_HAS_CHANGED, HKEY, online=online, sending_ts=ts()
        )

        mqtt_client.messages.put(
            MQTTMessage(msg.get_topic(), JsonPayloadEncoder().encode(msg.get_payload()))
        )
        await asyncio.sleep(0.001)

    def decode_node_message(self, msg: MQTTMessage):
        return Message.construct(
            msg.topic, ProtobufPayloadEncoder().decode(msg.payload)
        )[0]

    async def should_send_node_messages(self, mqtt_client, net, node):
        await node.establish_session()
        await asyncio.sleep(0.1)

        assert not mqtt_client.published

        await self.when_host_publish_state(mqtt_client, True)
        await asyncio.sleep(0.1)

        nbirth = self.decode_node_message(mqtt_client.published[0])
        assert nbirth.event == Event.NODE_IS_BORN
        assert nbirth.key == NKEY
        assert nbirth.metrics[-1].name == "Node/Temperature"

        await asyncio.sleep(0.1)
        dbirth = self.decode_node_message(mqtt_client.published[1])
        assert dbirth.event == Event.DEVICE_IS_BORN
        assert dbirth.key == DKEY
        assert dbirth.metrics[-1].name == "Device/Temperature"

        await net.write(NKEY, 100, alias=1)
        await asyncio.sleep(0.1)

        ndata = self.decode_node_message(mqtt_client.published[2])
        assert ndata.event == Event.NODE_DATA_HAS_CHANGED
        assert ndata.key == NKEY
        assert ndata.metrics[-1].alias == 1
        await self.when_host_publish_state(mqtt_client, False)

        await node.terminate_session()
        await asyncio.sleep(0.1)

        ndata = self.decode_node_message(mqtt_client.published[3])
        assert ndata.event == Event.DEVICE_HAS_DIED
        assert ndata.key == DKEY

        ndata = self.decode_node_message(mqtt_client.published[4])
        assert ndata.event == Event.NODE_HAS_DIED
        assert ndata.key == NKEY
        await self.when_host_publish_state(mqtt_client, False)

    async def should_execute_commands(self, mqtt_client, node, net):
        await node.establish_session()
        await asyncio.sleep(0.1)

        assert not mqtt_client.published

        await self.when_host_publish_state(mqtt_client, True)

        msg = Message(
            Event.NODE_COMMAND_IS_SENT,
            NKEY,
            [Metric(ts(), 10, DataType.Int16, name="Node/Temperature")],
            sending_ts=ts(),
        )
        mqtt_client.messages.put(
            MQTTMessage(
                msg.get_topic(), ProtobufPayloadEncoder().encode(msg.get_payload())
            )
        )
        await asyncio.sleep(0.02)

        mqtt_msg = mqtt_client.published[2]  # First Node Birth, second Device Birth
        msg = Message.construct(
            mqtt_msg.topic, ProtobufPayloadEncoder().decode(mqtt_msg.payload)
        )[0]
        assert msg.event == Event.NODE_DATA_HAS_CHANGED  # Confirmation
        assert msg.metrics[-1].value == 10
        assert await net.read(NKEY, alias=1) == 10

        await node.terminate_session()

    async def should_save_to_history_if_host_is_offline(self, node, mqtt_client, net):
        await node.establish_session()

        assert not mqtt_client.published

        await self.when_host_publish_state(mqtt_client, True)
        await asyncio.sleep(0.1)
        await self.when_host_publish_state(mqtt_client, False)

        await net.write(NKEY, 100, alias=1)
        await asyncio.sleep(0.1)

        await self.when_host_publish_state(mqtt_client, True)
        await net.write(NKEY, 50, alias=1)
        await asyncio.sleep(0.1)

        # 0: NBIRTH, 1: DBIRTH, // Host online // 2: NBIRTH, 3: DBIRTH
        ndata = self.decode_node_message(mqtt_client.published[4])
        assert ndata.event == Event.NODE_DATA_HAS_CHANGED
        assert ndata.key == NKEY
        assert len(ndata.metrics) == 2
        assert ndata.metrics[0].is_historical
        assert ndata.metrics[0].value == 100
        assert not ndata.metrics[1].is_historical
        assert ndata.metrics[1].value == 50

        await self.when_host_publish_state(mqtt_client, False)

        await node.terminate_session()

    async def should_save_to_history_if_desconnected(self, node, mqtt_client, net):
        await node.establish_session()

        assert not mqtt_client.published

        await self.when_host_publish_state(mqtt_client, True)
        await asyncio.sleep(0.1)
        mqtt_client.block_connection()

        await net.write(NKEY, 100, alias=1)
        await asyncio.sleep(0.1)

        mqtt_client.unblock_connection()
        await node.establish_session()
        await self.when_host_publish_state(mqtt_client, True)
        await net.write(NKEY, 50, alias=1)
        await asyncio.sleep(0.1)

        # 0: NBIRTH, 1: DBIRTH, // Host online // 2: NBIRTH, 3: DBIRTH
        # for msg in mqtt_client.published:
        #     print(self.decode_node_message(msg))
        ndata = self.decode_node_message(mqtt_client.published[4])
        assert ndata.event == Event.NODE_DATA_HAS_CHANGED
        assert ndata.key == NKEY
        assert len(ndata.metrics) == 2
        assert ndata.metrics[0].is_historical
        assert ndata.metrics[0].value == 100
        assert not ndata.metrics[1].is_historical
        assert ndata.metrics[1].value == 50

        await node.terminate_session()
