import asyncio
from unittest.mock import MagicMock

import pytest

import aiospb.hosts.hosts as h
from aiospb.hosts.commands import CommandFollower, MetricsCreator
from aiospb.hosts.values import (
    CommandRequest,
    CommandResponse,
    WriteRequest,
    WritingResolution,
)
from aiospb.messages import Event, Message, MessageSession, SessionIsBroken
from aiospb.shared import DataType, DeviceKey, HostOptions, Metric
from tests.fakes import FakeClock, Stack

HKEY = DeviceKey("hostname")
NKEY = DeviceKey("group/node")
TS = 1000
WRITE_TIMEOUT = 10
REQ = CommandRequest(NKEY, [WriteRequest(1, "Metric/Name")], WRITE_TIMEOUT)
OPTS = HostOptions()
CMD = Message(
    Event.NODE_COMMAND_IS_SENT,
    NKEY,
    [Metric(TS, 1, DataType.Int16, name="Metric/Name")],
)
METRICS = [Metric(TS, 1, DataType.Int16, alias=1, name="Metric/Name")]
MSG = Message(Event.NODE_IS_BORN, NKEY, METRICS, sending_ts=TS)


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


@pytest.fixture
def session():
    m = MagicMock(spec=MessageSession)
    m.messages = asyncio.Queue()

    async def subscribe():
        while True:
            msg = await m.messages.get()
            if isinstance(msg, Exception):
                raise msg
            yield msg

    m.subscribe.side_effect = subscribe
    return m


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


@pytest.fixture
def creator():
    return MagicMock(spec=MetricsCreator)


@pytest.fixture
def follower():
    return MagicMock(spec=CommandFollower)


@pytest.fixture
async def host(session, creator, follower, clock):
    srv = h.Host(HKEY, session, creator, follower, OPTS, clock)
    yield srv
    await srv.terminate_session()


class A_Host:
    async def should_establish_and_terminate_session(self, host, session):
        await host.establish_session()
        session.establish.assert_called()

        await host.terminate_session()
        session.terminate.assert_called()

    async def should_notify_to_creator_and_follower(
        self, host, session, follower, creator
    ):
        await host.establish_session()

        session.messages.put_nowait(MSG)
        await asyncio.sleep(0.01)

        follower.update_command_pipes.assert_called_with(MSG)
        creator.update_metric_pars.assert_called_with(MSG)

    async def should_notify_to_external_observers(self, host, session):
        val = []

        async def observe(msg: Message):
            val.append(msg)

        host.add_observer("custom", observe)
        await host.establish_session()

        session.messages.put_nowait(MSG)
        await asyncio.sleep(0.1)

        assert val == [MSG]

    async def should_cancel_slow_observers(self, session, logger, host):
        val = []

        host.with_options(HostOptions(notify_timeout=0.02))

        async def observe(_: Message):
            await asyncio.sleep(1)

        host.add_observer("custom", observe)
        await host.establish_session()

        session.messages.put_nowait(MSG)
        await asyncio.sleep(0.1)

        assert not val
        logger.warning.assert_called()

    async def should_notify_session_to_observers_session_is_broken(self, host, session):
        val = []

        async def observe(msg: Message):
            val.append(msg)

        host.add_observer("custom", observe)

        await host.establish_session()

        session.messages.put_nowait(SessionIsBroken())
        await asyncio.sleep(0.01)

        assert val[0] == Message(Event.HOST_STATE_HAS_CHANGED, HKEY, online=False)

    async def should_continue_session_if_not_mqtt_error(self, host, session, logger):
        await host.establish_session()

        session.messages.put_nowait(Exception("simulation of any exception"))
        await asyncio.sleep(0.01)

        logger.warning.assert_called()
        logger.exception.assert_called()

    async def should_rebirth_nodes_out_of_sequence(self, host, session, logger):
        await host.establish_session()

        out = Message(Event.OUT_OF_SEQUENCE, NKEY, [], sending_ts=TS)
        session.messages.put_nowait(out)
        await asyncio.sleep(0)

        logger.warning.assert_called()
        session.publish.assert_called_with(
            Message(
                Event.NODE_COMMAND_IS_SENT,
                NKEY,
                [Metric(TS, True, DataType.Boolean, name="Node Control/Rebirth")],
            )
        )

    async def should_process_commands_when_host_is_offline(self, host, session):
        session.is_established.return_value = False

        assert await host.execute_command(REQ) == CommandResponse(
            TS, [WritingResolution.HostIsOffline]
        )

    async def should_process_commands_whithout_data_types(self, host, creator):
        await host.establish_session()

        creator.construct_writing_metrics.return_value = [
            Metric(TS, 1, DataType.Unknown)
        ]

        assert await host.execute_command(REQ) == CommandResponse(
            TS, [WritingResolution.MetricNotFound]
        )

    async def should_send_commands_to_broker(self, host, creator, session):
        await host.establish_session()
        creator.construct_writing_metrics.return_value = CMD.metrics

        await host.execute_command(REQ)

        session.publish.assert_called_with(CMD)

    async def should_follow_command_execution(self, host, creator, follower):
        await host.establish_session()
        creator.construct_writing_metrics.return_value = CMD.metrics

        assert await host.execute_command(REQ) == follower.follow_command.return_value

        follower.follow_command.assert_called_with(CMD.key, CMD.metrics, WRITE_TIMEOUT)

    async def should_avoid_send_command_with_many_metrics_and_rebirth(
        self, host, creator, follower
    ):
        await host.establish_session()
        creator.construct_writing_metrics.return_value = CMD.metrics + [
            Metric(1000, True, DataType.Boolean, name="Node Control/Rebirth")
        ]

        assert await host.execute_command(REQ) == CommandResponse(
            TS, [WritingResolution.BadWriting] * (len(CMD.metrics) + 1)
        )
        follower.follow_command.assert_not_called()

    async def should_send_control_command_without_following(
        self, host, creator, follower
    ):
        await host.establish_session()
        creator.construct_writing_metrics.return_value = [
            Metric(1000, True, DataType.Boolean, name="Node Control/Rebirth")
        ]

        assert await host.execute_command(REQ) == CommandResponse(
            TS, [WritingResolution.GoodWriting]
        )

        follower.follow_command.assert_not_called()

    async def should_terminate_session_if_session_error_when_sending_command(
        self, host, creator, session
    ):
        await host.establish_session()
        creator.construct_writing_metrics.return_value = CMD.metrics
        session.publish.side_effect = SessionIsBroken()

        assert await host.execute_command(REQ) == CommandResponse(
            TS, [WritingResolution.HostIsOffline]
        )
