import asyncio
from dataclasses import dataclass
from unittest.mock import MagicMock, call

import pytest

import aiospb.messages.nodes as n
from aiospb.messages.ports import (
    MQTT5Client,
    MQTTError,
    PayloadEncoder,
    SessionIsBroken,
)
from aiospb.messages.values import Event, Message, MQTTMessage, Payload
from aiospb.shared import DataType, DeviceKey, Metric, NodeOptions
from tests.aiospb.messages.fakes import Stack, SubscriptionObserver
from tests.fakes import FakeClock

from .fakes import Stack, SubscriptionObserver

TS = 1000
HKEY = DeviceKey("hostname")
NKEY = DeviceKey("group/node")


METRICS = [
    Metric(1000, idx, DataType.Int16, alias=idx, name=f"Metric/{idx}")
    for idx in range(10)
]
NKEY = DeviceKey("group/node")
DKEY = DeviceKey("group/node/device")
HKEY = DeviceKey("hostname")
TS = 1000

OPTS = n.NodeOptions()


@pytest.fixture
def mqtt_client():
    m = MagicMock(spec=MQTT5Client)
    m.messages = Stack()
    return m


@pytest.fixture
def host_encoder():
    return MagicMock(spec=PayloadEncoder)


@pytest.fixture
def node_encoder():
    return MagicMock(spec=PayloadEncoder)


@pytest.fixture
def clock():
    return FakeClock(TS)


@pytest.fixture
async def session(mqtt_client, node_encoder, host_encoder, clock):
    s = n.NodeMessageSession(NKEY, mqtt_client, node_encoder, host_encoder, OPTS, clock)
    await s.establish()
    assert s.is_established()
    yield s
    await s.terminate()


@pytest.fixture
def logger(monkeypatch):
    mock = MagicMock()
    monkeypatch.setattr(n, "logger", mock)
    yield mock


class A_NodeMessageSession:
    async def should_publish_messages(self, session, mqtt_client, node_encoder):
        payload_bytes = b"payload"
        node_encoder.encode.return_value = payload_bytes
        birth = Metric(TS, 0, DataType.Int64, name="bdSeq")

        @dataclass
        class Case:
            name: str
            msg: Message
            mqtt_msg: MQTTMessage
            payload: Payload

        cases = [
            Case(
                "NBIRTH",
                Message(Event.NODE_IS_BORN, NKEY, METRICS),
                MQTTMessage("spBv1.0/group/NBIRTH/node", payload_bytes),
                Payload(TS, METRICS + [birth], 0),
            ),
            Case(
                "DBIRTH",
                Message(Event.DEVICE_IS_BORN, DKEY, METRICS),
                MQTTMessage("spBv1.0/group/DBIRTH/node/device", payload_bytes),
                Payload(TS, METRICS, 1),
            ),
            Case(
                "NDATA",
                Message(Event.NODE_DATA_HAS_CHANGED, NKEY, METRICS),
                MQTTMessage("spBv1.0/group/NDATA/node", payload_bytes),
                Payload(TS, METRICS, 2),
            ),
            Case(
                "DDATA",
                Message(Event.DEVICE_DATA_HAS_CHANGED, DKEY, METRICS),
                MQTTMessage("spBv1.0/group/DDATA/node/device", payload_bytes),
                Payload(TS, METRICS, 3),
            ),
            Case(
                "DDEATH",
                Message(Event.DEVICE_HAS_DIED, DKEY),
                MQTTMessage("spBv1.0/group/DDEATH/node/device", payload_bytes),
                Payload(TS, seq=4),
            ),
            Case(
                "NDEATH",
                Message(Event.NODE_HAS_DIED, NKEY),
                MQTTMessage("spBv1.0/group/NDEATH/node", payload_bytes, qos=1),
                Payload(TS, [birth]),
            ),
        ]

        for c in cases:
            await session.publish(c.msg)
            assert call(c.mqtt_msg) == mqtt_client.publish.mock_calls[-1]
            assert call(c.payload) == node_encoder.encode.mock_calls[-1]
            assert c.msg.sending_ts == TS

    async def should_increase_sequence_circular_till_255(self, session, node_encoder):
        for _ in range(257):
            await session.publish(Message(Event.NODE_IS_BORN, NKEY, METRICS))

        assert node_encoder.encode.mock_calls[-1].args[0].seq == 0

    async def should_increase_bd_seq_sequentially(
        self, session, node_encoder, mqtt_client
    ):
        for _ in range(256):
            await session.publish(Message(Event.NODE_IS_BORN, NKEY, METRICS))

            mqtt_client.publish.side_effect = MQTTError(Exception("connection broken"))
            try:
                await session.publish(Message(Event.NODE_IS_BORN, NKEY, METRICS))
            except SessionIsBroken:
                await session.establish()
                mqtt_client.publish.side_effect = None

        assert node_encoder.encode.mock_calls[-1].args[0].metrics[-1].value == 0

    async def should_split_data_messages_with_over_size(
        self, session, node_encoder, mqtt_client
    ):
        session.with_options(NodeOptions(max_payload_size=50))

        node_encoder.encode.side_effect = (
            lambda payload: b"fixed-payload" + b"+metric-payload" * len(payload.metrics)
        )
        await session.publish(Message(Event.NODE_DATA_HAS_CHANGED, NKEY, METRICS))

        assert len(mqtt_client.publish.mock_calls) == 5

    async def should_not_birth_messages_with_over_size(self, session, node_encoder):
        session.with_options(NodeOptions(max_payload_size=50))

        node_encoder.encode.side_effect = (
            lambda payload: b"fixed-payload" + b"+metric-payload" * len(payload.metrics)
        )
        with pytest.raises(ValueError):
            await session.publish(Message(Event.NODE_IS_BORN, NKEY, METRICS))

    async def should_notify_host_state_messages(
        self, mqtt_client, host_encoder, session
    ):
        async with SubscriptionObserver(session.subscribe()) as queue:
            host_encoder.decode.return_value = Payload(TS, online=True)
            mqtt_client.messages.put(MQTTMessage("spBv1.0/STATE/hostname", b"payload"))

            msg = await queue.get()
            assert msg == Message(
                Event.HOST_STATE_HAS_CHANGED,
                DeviceKey("hostname"),
                online=True,
                sending_ts=TS,
            )

    async def should_notify_command_messages(self, mqtt_client, node_encoder, session):
        async with SubscriptionObserver(session.subscribe()) as queue:
            node_encoder.decode.return_value = Payload(TS, METRICS)
            mqtt_client.messages.put(MQTTMessage("spBv1.0/group/NCMD/node", b"payload"))

            msg = await queue.get()
            assert msg == Message(
                Event.NODE_COMMAND_IS_SENT,
                NKEY,
                METRICS,
                sending_ts=TS,
            )

    async def should_not_notify_command_messages_with_wrong_keys(
        self, mqtt_client, node_encoder, session, logger
    ):
        async with SubscriptionObserver(session.subscribe()) as queue:
            node_encoder.decode.return_value = Payload(TS, METRICS)
            mqtt_client.messages.put(
                MQTTMessage("spBv1.0/wrong-group/NCMD/node", b"payload")
            )
            await asyncio.sleep(0.01)

            assert queue.qsize() == 0
            logger.warning.assert_called()

    async def should_not_notify_node_messages(
        self, mqtt_client, node_encoder, session, logger
    ):
        async with SubscriptionObserver(session.subscribe()) as queue:
            node_encoder.decode.return_value = Payload(TS, METRICS)
            mqtt_client.messages.put(
                MQTTMessage("spBv1.0/group/NBIRTH/node", b"payload")
            )
            await asyncio.sleep(0.01)

            assert queue.qsize() == 0
            logger.warning.assert_called()
