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

import pytest

from aiospb.hosts.commands import (
    CommandFollower,
    CommandRequest,
    CommandResponse,
    MetricsCreator,
    WritingResolution,
)
from aiospb.hosts.values import WriteRequest
from aiospb.infra.inmem import InMemMinMetricInfoCache
from aiospb.messages import Event, Message
from aiospb.shared import DataType, DeviceKey, Metric, Quality
from tests.builders import MetricBuilder
from tests.fakes import FakeClock

NKEY = DeviceKey("group/node")
DKEY = DeviceKey("group/node/device")
TS = 1000
METRICS = [
    MetricBuilder()
    .with_name("Integer/With Alias 1")
    .with_alias(1)
    .with_value(1)
    .with_datatype("Int16")
    .build(),
    MetricBuilder()
    .with_name("String/Without Alias")
    .with_datatype("String")
    .with_alias(0)
    .with_value("value")
    .build(),
]


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


@pytest.fixture
def cache():
    return InMemMinMetricInfoCache()


@pytest.fixture
def creator(cache, clock):
    return MetricsCreator(cache, clock)


class A_MetricsCreator:
    def given_a_request(self, key):
        return CommandRequest(
            key,
            [
                WriteRequest(5, METRICS[0].name),
                WriteRequest("new-value", METRICS[1].name),
            ],
        )

    async def should_create_a_node_command_without_datatype(self, creator):
        req = self.given_a_request(NKEY)

        assert await creator.construct_writing_metrics(req) == [
            Metric(TS, 5, DataType.Unknown, 0, METRICS[0].name),
            Metric(TS, "new-value", DataType.Unknown, 0, METRICS[1].name),
        ]

    async def should_create_a_device_command_without_datatype(self, creator):
        req = self.given_a_request(DKEY)

        assert await creator.construct_writing_metrics(req) == [
            Metric(TS, 5, DataType.Unknown, 0, METRICS[0].name),
            Metric(TS, "new-value", DataType.Unknown, 0, METRICS[1].name),
        ]

    async def should_create_a_command_with_correct_data_type(self, creator):
        await creator.update_metric_pars(Message(Event.NODE_IS_BORN, NKEY, METRICS))
        req = CommandRequest(NKEY, [WriteRequest("new-value", "String/Without Alias")])

        assert await creator.construct_writing_metrics(req) == [
            Metric(TS, "new-value", DataType.String, name="String/Without Alias")
        ]

    async def should_create_a_command_with_compresed_metric(self, creator):
        await creator.update_metric_pars(Message(Event.NODE_IS_BORN, NKEY, METRICS))
        req = CommandRequest(NKEY, [WriteRequest(5, "Integer/With Alias 1")])

        assert await creator.construct_writing_metrics(req) == [
            Metric(TS, 5, DataType.Int16, alias=1)
        ]

    async def should_unknown_data_types_if_death(self, creator):
        await creator.update_metric_pars(Message(Event.NODE_IS_BORN, NKEY, METRICS))
        await creator.update_metric_pars(Message(Event.NODE_HAS_DIED, NKEY))

        req = CommandRequest(NKEY, [WriteRequest(5, "Integer/With Alias 1")])

        assert await creator.construct_writing_metrics(req) == [
            Metric(TS, 5, DataType.Unknown, name="Integer/With Alias 1")
        ]


@pytest.fixture
def follower(clock):
    return CommandFollower(clock)


class A_CommandFollower:
    async def should_resolve_good_writing(self, follower):
        metrics = [MetricBuilder().build()]
        await follower.update_command_pipes(Message(Event.NODE_IS_BORN, NKEY, metrics))

        @dataclass
        class Case:
            metric: Metric
            resolution: WritingResolution

        cases = [
            Case(MetricBuilder().build(), WritingResolution.GoodWriting),
            Case(
                MetricBuilder().with_quality(Quality.GOOD).build(),
                WritingResolution.GoodWriting,
            ),
            Case(
                MetricBuilder().with_quality(Quality.BAD).build(),
                WritingResolution.BadWriting,
            ),
            Case(
                MetricBuilder().with_quality(Quality.STALE).build(),
                WritingResolution.StaleWriting,
            ),
        ]
        for c in cases:
            task = asyncio.create_task(follower.follow_command(NKEY, metrics))
            await asyncio.sleep(0.01)

            assert not task.done()

            await follower.update_command_pipes(
                Message(Event.NODE_DATA_HAS_CHANGED, NKEY, [c.metric])
            )

            assert await task == CommandResponse(TS, [c.resolution])

    async def should_ignore_wrong_confirmations(self, follower):
        metrics = [MetricBuilder().build()]
        await follower.update_command_pipes(Message(Event.NODE_IS_BORN, NKEY, metrics))

        cases = [
            [MetricBuilder().build(), MetricBuilder().build()],  # incorrect length
            [MetricBuilder().with_name("Incorrect/Name").build()],
            [MetricBuilder().with_value(10000).build()],  # incorrect value
        ]

        task = asyncio.create_task(follower.follow_command(NKEY, metrics))
        for c in cases:
            await follower.update_command_pipes(
                Message(Event.NODE_DATA_HAS_CHANGED, NKEY, c)
            )

        await asyncio.sleep(0.01)
        assert not task.done()

        await follower.update_command_pipes(Message(Event.NODE_HAS_DIED, NKEY))

    async def should_resolve_node_is_disconnected(self, follower):
        metrics = [MetricBuilder().build()]
        await follower.update_command_pipes(Message(Event.NODE_IS_BORN, NKEY, metrics))

        await follower.update_command_pipes(Message(Event.NODE_HAS_DIED, NKEY))

        assert await follower.follow_command(NKEY, metrics) == CommandResponse(
            TS, [WritingResolution.EdgeIsOffline]
        )

    async def should_resolve_timeout(self, follower):
        metrics = [MetricBuilder().build()]
        await follower.update_command_pipes(Message(Event.NODE_IS_BORN, NKEY, metrics))

        task = asyncio.create_task(
            follower.follow_command(NKEY, metrics, timeout=0.005)
        )

        await asyncio.sleep(0.1)

        assert await task == CommandResponse(TS, [WritingResolution.WritingTimeout])
