import asyncio
from unittest.mock import MagicMock

import pytest

import aiospb.nodes.devices as d
from aiospb.infra.inmem import InMemReadingStore
from aiospb.messages import Event, Message
from aiospb.nodes.ports import DeviceConnectionIsBroken, MetricsNetwork
from aiospb.nodes.scanning import Scanner
from aiospb.nodes.values import MetricInfo, Reading
from aiospb.shared import (
    DataType,
    DeviceKey,
    Metric,
    PropertySet,
    PropertyValue,
    Quality,
    SparkplugBError,
)
from tests.fakes import FakeClock

TS = 1000
DKEY = DeviceKey("group/node/device")
SCAN_RATE = 60000  # 1 min
WRITE_WAIT = 10000


@pytest.fixture
def scanner(monkeypatch):
    mock = MagicMock(spec=Scanner)
    monkeypatch.setattr(d, "Scanner", MagicMock(return_value=mock))
    return mock


@pytest.fixture
def net():
    return MagicMock(spec=MetricsNetwork)


@pytest.fixture
def readings():
    return InMemReadingStore()


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


@pytest.fixture
async def device(net, readings, clock):
    dev = d.NodeDevice(DKEY, net, readings, clock)
    yield dev
    await dev.die()


class A_Device:
    async def recieve_message(self, message):
        if not hasattr(self, "messages"):
            self.messages = asyncio.Queue()
        await self.messages.put(message)

    async def should_connect_to_when_rise(self, device, net):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16)]

        await device.rise(SCAN_RATE)

        net.connect_device.assert_called_with(device.key)

    async def should_disconnect_to_when_die(self, device, net):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16)]

        await device.die()

        net.disconnect_device.assert_called_with(device.key)

    async def should_notify_birth(self, device, net):
        device.add_node_notification(self.recieve_message)

        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16)]
        net.read.return_value = 1
        await device.rise(SCAN_RATE)

        assert self.messages.qsize() > 0
        assert self.messages.get_nowait() == Message(
            Event.DEVICE_IS_BORN,
            DKEY,
            [
                Metric(TS, False, DataType.Boolean, name="Device Control/Rebirth"),
                Metric(TS, False, DataType.Boolean, name="Device Control/Restart"),
                Metric(TS, SCAN_RATE, DataType.Int32, name="Device Control/Scan Rate"),
                Metric(TS, 1, DataType.Int16, name="Metric/Name"),
            ],
        )

    async def should_notify_birth_stales(self, device, net):
        device.add_node_notification(self.recieve_message)

        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16)]
        net.read.side_effect = DeviceConnectionIsBroken(DKEY, "conn")
        await device.rise(SCAN_RATE)

        assert self.messages.qsize() > 0
        metrics = [
            Metric(TS, False, DataType.Boolean, name="Device Control/Rebirth"),
            Metric(TS, False, DataType.Boolean, name="Device Control/Restart"),
            Metric(TS, SCAN_RATE, DataType.Int32, name="Device Control/Scan Rate"),
            Metric(TS, None, DataType.Int16, name="Metric/Name"),
        ]
        metrics[-1].quality = Quality.STALE

        msg = self.messages.get_nowait()
        assert msg == Message(
            Event.DEVICE_IS_BORN,
            DKEY,
            metrics,
        )

    async def should_create_a_default_scanner(self, device, net, scanner, readings):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16, alias=1)]
        net.read.return_value = 1

        await device.rise(SCAN_RATE)

        scanner.start.assert_called_with(SCAN_RATE)

        await device.die()
        scanner.stop.assert_called_with()
        assert await readings.get([1]) == [
            Reading("Metric/Name", 1, 1, DataType.Int16, Quality.GOOD)
        ]

    async def should_not_create_any_scanner(self, device, net, scanner):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16)]
        net.read.return_value = 1

        await device.rise(SCAN_RATE)

        scanner.start.assert_not_called()

    async def should_create_a_network_defined_scanner(self, device, net, scanner):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [
            MetricInfo("Device Control/Scan Rate", DataType.Int32),
            MetricInfo("Metric/Name", DataType.Int16, alias=1),
        ]
        net.read.side_effect = [100000, 1]

        await device.rise(SCAN_RATE)

        scanner.start.assert_called_with(100000)

    async def should_ommit_scanning_in_a_metric(self, device, net, scanner):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [
            MetricInfo(
                "Metric/Name",
                DataType.Int16,
                properties=PropertySet({"scan_rate": PropertyValue(0, DataType.Int32)}),
                alias=1,
            ),
        ]
        net.read.return_value = 1
        await device.rise(SCAN_RATE)

        scanner.start.assert_not_called()

    async def should_follow_special_scan_rate_in_a_metric(self, device, net, scanner):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [
            MetricInfo(
                "Metric/Name",
                DataType.Int16,
                properties=PropertySet(
                    {"scan_rate": PropertyValue(10, DataType.Int32)}
                ),
                alias=1,
            ),
        ]
        net.read.return_value = 1

        await device.rise(SCAN_RATE)

        scanner.start.assert_called_with(10)

    async def should_dont_write_metricss_if_not_born(self, device):
        device.add_node_notification(self.recieve_message)

        with pytest.raises(SparkplugBError):
            await device.write_metrics(
                Message(
                    Event.DEVICE_COMMAND_IS_SENT,
                    DKEY,
                    [Metric(TS, 2, DataType.Int16, 1, "Metric/Name")],
                    sending_ts=TS,
                ),
                WRITE_WAIT,
            )

    async def should_write_metrics_with_good_result(self, device, net, readings, clock):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16, alias=1)]
        await device.rise(SCAN_RATE)

        net.write.return_value = 1
        await device.write_metrics(
            [Metric(TS, 2, DataType.Int16, 1, "Metric/Name")],
            WRITE_WAIT,
        )

        net.write.assert_called_with(DKEY, 2, alias=1, name="Metric/Name")
        assert await readings.get([1]) == [
            Reading("Metric/Name", 1, 2, DataType.Int16, Quality.GOOD)
        ]
        assert 10000 in clock.waits

    async def should_write_metrics_with_bad_result(self, device, net):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16, alias=1)]
        await device.rise(SCAN_RATE)

        net.write.return_value = 0
        net.write.side_effect = Exception("writing failed")
        await device.write_metrics(
            [
                Metric(TS, 2, DataType.Int16, 1, "Metric/Name 1"),
                Metric(TS, 2, DataType.Int16, 2, "Metric/Name 2"),
            ],
            WRITE_WAIT,
        )

        self.messages.get_nowait()  # clean birth
        assert self.messages.get_nowait() == Message(
            Event.DEVICE_DATA_HAS_CHANGED,
            DKEY,
            [
                Metric(
                    TS,
                    2,
                    DataType.Int16,
                    1,
                    "Metric/Name 1",
                    PropertySet(
                        {"quality": PropertyValue(Quality.BAD.value, DataType.Int32)}
                    ),
                ),
                Metric(
                    TS,
                    2,
                    DataType.Int16,
                    2,
                    "Metric/Name 2",
                    PropertySet(
                        {"quality": PropertyValue(Quality.BAD.value, DataType.Int32)}
                    ),
                ),
            ],
        )

    async def should_write_metrics_with_stale_result(
        self, device, net, readings, clock
    ):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16, alias=1)]
        await device.rise(SCAN_RATE)

        net.write.side_effect = TimeoutError()
        await device.write_metrics(
            [Metric(TS, 2, DataType.Int16, 1, "Metric/Name")], WRITE_WAIT
        )

        self.messages.get_nowait()  # clean birth
        assert self.messages.get_nowait() == Message(
            Event.DEVICE_DATA_HAS_CHANGED,
            DKEY,
            [
                Metric(
                    TS,
                    2,
                    DataType.Int16,
                    1,
                    "Metric/Name",
                    PropertySet(
                        {"quality": PropertyValue(Quality.STALE.value, DataType.Int32)}
                    ),
                ),
            ],
        )

    async def should_write_metrics_with_stale_result_before_run(
        self, device, net, readings, clock
    ):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16, alias=1)]
        await device.rise(SCAN_RATE)

        await device.write_metrics(
            [Metric(TS, 2, DataType.Int16, 1, "Metric/Name")],
            0,
        )

        self.messages.get_nowait()  # clean birth
        assert self.messages.get_nowait() == Message(
            Event.DEVICE_DATA_HAS_CHANGED,
            DKEY,
            [
                Metric(
                    TS,
                    2,
                    DataType.Int16,
                    1,
                    "Metric/Name",
                    PropertySet(
                        {"quality": PropertyValue(Quality.STALE.value, DataType.Int32)}
                    ),
                ),
            ],
        )

    async def should_write_scan_rate(self, device, net, scanner, clock):
        device.add_node_notification(self.recieve_message)
        net.get_info.return_value = [MetricInfo("Metric/Name", DataType.Int16, alias=1)]
        await device.rise(SCAN_RATE)
        await asyncio.sleep(0.01)

        await device.write_metrics(
            [Metric(TS, 600, DataType.Int32, name="Device Control/Scan Rate")],
            WRITE_WAIT,
        )

        scanner.start.assert_called_with(600)
        net.write.assert_called_with(
            DKEY, 600, alias=0, name="Device Control/Scan Rate"
        )
