import asyncio
from unittest.mock import MagicMock, call

import pytest
from aiomqtt import MqttError

import aiospb.messages.hosts as h
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
from tests.fakes import FakeClock

from .fakes import Stack, SubscriptionObserver

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 = h.HostOptions(group="group")


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


@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 = h.HostMessageSession(HKEY, mqtt_client, node_encoder, host_encoder, OPTS, clock)
    await s.establish()
    assert s.is_established()
    yield s
    await s.terminate()


class A_HostMessageSession:
    @pytest.mark.usefixtures("session")
    async def should_publish_host_is_online_when_establish(
        self, mqtt_client, host_encoder
    ):
        mqtt_msg = MQTTMessage(
            "spBv1.0/STATE/hostname", host_encoder.encode.return_value, 1, True
        )
        host_encoder.encode.assert_called_with(Payload(TS, online=True))
        mqtt_client.publish.assert_called_with(mqtt_msg)

    @pytest.mark.usefixtures("session")
    async def should_publish_will_if_accidentally_host_is_offline(
        self, mqtt_client, host_encoder
    ):
        mqtt_msg = MQTTMessage(
            "spBv1.0/STATE/hostname", host_encoder.encode.return_value, 1, True
        )
        mqtt_client.connect.assert_called_with(will=mqtt_msg)
        assert call(Payload(TS, online=False)) == host_encoder.encode.mock_calls[0]

    @pytest.mark.usefixtures("session")
    async def should_subscribe_to_node_messages(self, mqtt_client):
        mqtt_client.subscribe.assert_called_with("spBv1.0/group/+/+/#", qos=1)

    async def should_publish_host_is_offline_when_terminate(
        self, session, mqtt_client, host_encoder
    ):
        await session.terminate()

        mqtt_msg = MQTTMessage(
            "spBv1.0/STATE/hostname", host_encoder.encode.return_value, 1, True
        )
        host_encoder.encode.assert_called_with(Payload(TS, online=False))
        mqtt_client.publish.assert_called_with(mqtt_msg)

    async def should_publish_device_commands(self, session, mqtt_client, node_encoder):
        await session.publish(Message(Event.DEVICE_COMMAND_IS_SENT, DKEY, METRICS))

        mqtt_msg = MQTTMessage(
            "spBv1.0/group/DCMD/node/device", node_encoder.encode.return_value
        )
        mqtt_client.publish.assert_called_with(mqtt_msg)
        node_encoder.encode.assert_called_with(Payload(TS, METRICS))

    async def should_publish_node_commands(self, session, mqtt_client, node_encoder):
        await session.publish(Message(Event.NODE_COMMAND_IS_SENT, NKEY, METRICS))

        mqtt_msg = MQTTMessage(
            "spBv1.0/group/NCMD/node", node_encoder.encode.return_value
        )
        mqtt_client.publish.assert_called_with(mqtt_msg)
        node_encoder.encode.assert_called_with(Payload(TS, METRICS))

    async def should_break_session_when_establish_session(self, session, mqtt_client):
        mqtt_client.connect.side_effect = MQTTError(Exception("something bad happened"))

        with pytest.raises(SessionIsBroken):
            await session.establish()

    async def should_break_session_when_publish(self, session, mqtt_client):
        mqtt_client.publish.side_effect = MQTTError(Exception("something bad happened"))

        with pytest.raises(SessionIsBroken):
            await session.publish(Message(Event.DEVICE_COMMAND_IS_SENT, NKEY, METRICS))

    async def should_break_session_when_subscribe(self, session, mqtt_client):
        mqtt_client.messages.put(MQTTError(Exception("something bad happened")))
        await asyncio.sleep(0.01)

        async def test_sub():
            async for msg in session.subscribe():
                return msg

        with pytest.raises(SessionIsBroken):
            await asyncio.wait_for(test_sub(), timeout=0.01)

    async def when_birth_is_published_by_node(self, seq, bd_seq):
        bd_metric = Metric(TS, bd_seq, DataType.Int16, name="bdSeq")
        msg = Message(Event.NODE_IS_BORN, NKEY, METRICS, sending_ts=TS)
        self.nencoder.decode.return_value = Payload(TS, METRICS + [bd_metric], seq)
        self.mqtt_client.messages.put(
            MQTTMessage("spBv1.0/group/NBIRTH/node", b"birth-payload", 0, False)
        )
        await asyncio.sleep(0.005)
        return msg

    async def when_data_is_published_by_node(self, seq):
        msg = Message(Event.NODE_DATA_HAS_CHANGED, NKEY, METRICS, sending_ts=TS)
        self.nencoder.decode.return_value = Payload(TS, METRICS, seq)
        self.mqtt_client.messages.put(
            MQTTMessage("spBv1.0/group/NDATA/node", b"data-payload", 0, False)
        )
        await asyncio.sleep(0.005)
        return msg

    async def when_any_message_is_published_by_device(self, seq, event):
        metric = Metric(TS, seq, DataType.Int16, name="mySeq")
        msg = Message(
            Event.NODE_DATA_HAS_CHANGED, DKEY, METRICS + [metric], sending_ts=TS
        )
        self.nencoder.decode.return_value = Payload(TS, METRICS + [metric], seq)
        self.mqtt_client.messages.put(
            MQTTMessage(
                f"spBv1.0/group/{event.as_message_type}/node", b"data-payload", 0, False
            )
        )
        await asyncio.sleep(0.005)
        return msg

    async def when_dead_is_published_by_node(self, bd_seq):
        bd_metric = Metric(TS, bd_seq, DataType.Int16, name="bdSeq")
        msg = Message(Event.NODE_IS_BORN, NKEY, METRICS, sending_ts=TS)
        self.nencoder.decode.return_value = Payload(TS, [bd_metric])
        self.mqtt_client.messages.put(
            MQTTMessage("spBv1.0/group/NDEATH/node", b"death-payload", 0, False)
        )
        await asyncio.sleep(0.005)
        return msg

    async def should_pass_mqtt_message(self, session, mqtt_client, node_encoder):
        self.mqtt_client = mqtt_client
        self.nencoder = node_encoder
        async with SubscriptionObserver(session.subscribe()) as queue:
            expected = await self.when_birth_is_published_by_node(seq=0, bd_seq=0)
            msg = await asyncio.wait_for(queue.get(), timeout=0.5)
            assert expected == msg
            node_encoder.decode.assert_called_with(b"birth-payload")

    async def should_correct_wrong_message_sequence(
        self, session, mqtt_client, node_encoder
    ):
        self.mqtt_client = mqtt_client
        self.nencoder = node_encoder
        async with SubscriptionObserver(session.subscribe()) as queue:
            expected_first = await self.when_birth_is_published_by_node(seq=0, bd_seq=0)
            expected_third = await self.when_data_is_published_by_node(seq=2)
            expected_second = await self.when_data_is_published_by_node(seq=1)

            assert await queue.get() == expected_first
            assert await queue.get() == expected_second
            assert await queue.get() == expected_third

    async def should_send_out_of_sequence_message(
        self, session, mqtt_client, node_encoder
    ):
        self.mqtt_client = mqtt_client
        self.nencoder = node_encoder

        h.HostOptions(reorder_time=0.01).apply(session)
        async with SubscriptionObserver(session.subscribe()) as queue:
            expected_first = await self.when_birth_is_published_by_node(seq=0, bd_seq=0)
            await self.when_data_is_published_by_node(seq=5)
            await asyncio.sleep(0.02)

            assert await queue.get() == expected_first
            out_of_sequence = await queue.get()
            assert out_of_sequence.event == Event.OUT_OF_SEQUENCE

    async def should_log_death_out_of_sequence(
        self, session, mqtt_client, node_encoder, logger
    ):
        self.mqtt_client = mqtt_client
        self.nencoder = node_encoder

        async with SubscriptionObserver(session.subscribe()) as queue:
            await self.when_dead_is_published_by_node(bd_seq=0)
            assert queue.qsize() == 0

            logger.warning.assert_called()

    async def should_log_lost_of_pending_messages_after_rebirth(
        self, session, mqtt_client, node_encoder, logger
    ):
        self.mqtt_client = mqtt_client
        self.nencoder = node_encoder

        async with SubscriptionObserver(session.subscribe()) as queue:
            await self.when_birth_is_published_by_node(0, 0)
            await self.when_data_is_published_by_node(2)
            await self.when_birth_is_published_by_node(5, 0)

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

    async def should_log_lost_of_changes_by_no_birth(
        self, session, mqtt_client, node_encoder, logger
    ):
        self.mqtt_client = mqtt_client
        self.nencoder = node_encoder

        async with SubscriptionObserver(session.subscribe()) as queue:
            await self.when_data_is_published_by_node(2)

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

    async def should_log_incorrect_sequences_of_births(
        self, session, mqtt_client, node_encoder, logger
    ):
        self.mqtt_client = mqtt_client
        self.nencoder = node_encoder

        async with SubscriptionObserver(session.subscribe()) as queue:
            await self.when_birth_is_published_by_node(0, 1)
            await self.when_birth_is_published_by_node(3, 1)

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