import asyncio
import datetime
import struct
from typing import Type
from zoneinfo import ZoneInfo

import freezegun
import pytest
from bleak import BleakClient
from mockito import ANY, expect, mock, verify, when

import melnor_bluetooth.device as device_module
from melnor_bluetooth.constants import (
    BATTERY_UUID,
    MANUFACTURER_UUID,
    VALVE_MANUAL_SETTINGS_UUID,
    VALVE_MANUAL_STATES_UUID,
)
from melnor_bluetooth.device import Device, Valve
from tests.constants import TEST_UUID

zone_manual_setting_bytes = struct.pack(
    ">?HH?HH?HH?HH",
    # Valve 1
    True,
    5,
    5,
    # Valve 2
    True,
    10,
    10,
    # Valve 3
    True,
    15,
    15,
    # Valve 4
    True,
    20,
    20,
)


@pytest.fixture
def client_mock() -> Type:
    connect = asyncio.Future()
    connect.set_result(True)

    client_mock = mock(spec=BleakClient)

    when(device_module).BleakClient(TEST_UUID, disconnected_callback=ANY).thenReturn(
        client_mock
    )

    read_manufacturer = asyncio.Future()
    read_manufacturer.set_result(b"111110400")
    when(client_mock).read_gatt_char(MANUFACTURER_UUID).thenReturn(read_manufacturer)

    when(client_mock).connect(timeout=60).thenReturn(connect)

    return client_mock


class TestValveZone:
    def test_zone_update_state(self, client_mock):
        device = Device(mac=TEST_UUID)

        device.zone1.update_state(zone_manual_setting_bytes, VALVE_MANUAL_SETTINGS_UUID)

        assert device.zone1.is_watering == True
        assert device.zone1.manual_watering_minutes == 5

    def test_zone_properties(self, client_mock):
        device = Device(mac=TEST_UUID)

        device.zone1.is_watering = True

        assert device.zone1.manual_watering_minutes == 20

    def test_zone_defaults(self, client_mock):
        device = Device(mac=TEST_UUID)

        zone = Valve(0, device)

        assert zone.is_watering == False
        assert zone.manual_watering_minutes == 20

    def test_zone_byte_payload(self, client_mock):
        device = Device(mac=TEST_UUID)
        zone = Valve(0, device)

        zone.is_watering = True
        zone.manual_watering_minutes = 10

        assert zone._manual_setting_bytes() == b"\x01\x00\n\x00\n"  # type: ignore


class TestDevice:
    async def test_properties(self, client_mock):

        device = Device(mac=TEST_UUID)

        await device.connect()

        assert device.name == "4 Valve Timer"
        assert device.model == "11111"
        assert device.mac == TEST_UUID
        assert device.valve_count == 4

    async def test_get_item(self, client_mock):
        device = Device(mac=TEST_UUID)

        await device.connect()

        assert device["zone1"] is device.zone1
        assert device["zone2"] is device.zone2
        assert device["zone3"] is device.zone3
        assert device["zone4"] is device.zone4

    async def test_1_valve_device(self, client_mock):

        read_manufacturer = asyncio.Future()
        read_manufacturer.set_result(b"111110100")
        when(client_mock).read_gatt_char(MANUFACTURER_UUID).thenReturn(
            read_manufacturer
        )

        device = Device(mac=TEST_UUID)

        await device.connect()

        assert device.zone1 is not None
        assert device.zone2 is None
        assert device.zone3 is None
        assert device.zone4 is None

    async def test_2_valve_device(self, client_mock):

        device = Device(mac=TEST_UUID)

        read_manufacturer = asyncio.Future()
        read_manufacturer.set_result(b"111110200")

        when(client_mock).read_gatt_char(MANUFACTURER_UUID).thenReturn(
            read_manufacturer
        )

        await device.connect()

        assert device.zone1 is not None
        assert device.zone2 is not None
        assert device.zone3 is None
        assert device.zone4 is None

    async def test_1_valve_has_all_bytes(self, client_mock):

        read_manufacturer = asyncio.Future()
        read_manufacturer.set_result(b"111110100")
        when(client_mock).read_gatt_char(MANUFACTURER_UUID).thenReturn(
            read_manufacturer
        )

        device = Device(mac=TEST_UUID)

        await device.connect()

        device.zone1.is_watering = True
        device.zone1.manual_watering_minutes = 10

        assert (
            (
                device._valves[0]._manual_setting_bytes()  # type:ignore
                + device._valves[1]._manual_setting_bytes()  # type:ignore
                + device._valves[2]._manual_setting_bytes()  # type:ignore
                + device._valves[3]._manual_setting_bytes()  # type:ignore
            )
            == b"\x01\x00\n\x00\n\x00\x00\x14\x00\x14\x00\x00\x14\x00\x14\x00\x00\x14\x00\x14"  # noqa: E501
        )

    async def test_1_valve_has_internal_valves(self, client_mock):

        read_manufacturer = asyncio.Future()
        read_manufacturer.set_result(b"111110100")
        when(client_mock).read_gatt_char(MANUFACTURER_UUID).thenReturn(
            read_manufacturer
        )

        device = Device(mac=TEST_UUID)

        await device.connect()

        device.zone1.is_watering = True
        device.zone1.manual_watering_minutes = 10

        assert device.zone1 is not None
        assert device.zone2 is None
        assert device.zone3 is None
        assert device.zone4 is None

        assert device._valves[1] is not None  # type:ignore
        assert device._valves[2] is not None  # type:ignore
        assert device._valves[3] is not None  # type:ignore

    async def test_device_connect_lock(self, client_mock):
        with expect(BleakClient, times=1).connect():
            device = Device(mac=TEST_UUID)

            success = asyncio.Future()

            when(BleakClient).connect().thenReturn(success)
            when(device_module).BleakClient(
                TEST_UUID, disconnected_callback=ANY
            ).thenReturn(client_mock)

            read_manufacturer = asyncio.Future()
            read_manufacturer.set_result(b"111110100")
            when(client_mock).read_gatt_char(MANUFACTURER_UUID).thenReturn(
                read_manufacturer
            )

            # We'll verify we only call the bleak client connect once
            loop = asyncio.get_event_loop()
            one = loop.create_task(device.connect())
            two = loop.create_task(device.connect())
            three = loop.create_task(device.connect())
            four = loop.create_task(device.connect())

            success.set_result(True)

            # await the tasks to ensure they're done
            await asyncio.gather(one, two, three, four)

    async def test_device_connect_noop_when_connected(self, client_mock):
        with expect(BleakClient, times=1).connect():

            device = Device(mac=TEST_UUID)

            success = asyncio.Future()
            success.set_result(True)

            when(BleakClient).connect().thenReturn(success)
            when(device_module).BleakClient(
                TEST_UUID, disconnected_callback=ANY
            ).thenReturn(client_mock)

            read_manufacturer = asyncio.Future()
            read_manufacturer.set_result(b"111110100")
            when(client_mock).read_gatt_char(MANUFACTURER_UUID).thenReturn(
                read_manufacturer
            )

            await device.connect()
            await device.connect()
            await device.connect()
            await device.connect()

    @freezegun.freeze_time(datetime.datetime.now(tz=ZoneInfo("UTC")))
    async def test_fetch(self, client_mock):
        device = Device(mac=TEST_UUID)

        read_battery = asyncio.Future()
        read_battery.set_result(b"\x02\x85")

        read_manufacturer = asyncio.Future()
        read_manufacturer.set_result(b"111110400")

        read_manual_settings = asyncio.Future()
        read_manual_settings.set_result(
            struct.pack(
                ">?HH?HH?HH?HH",
                # Valve 1
                False,
                0,
                0,
                # Valve 2
                False,
                0,
                0,
                # Valve 3
                False,
                0,
                0,
                # Valve 4
                False,
                0,
                0,
            )
        )

        read_manual_state = asyncio.Future()
        read_manual_state.set_result(
            struct.pack(
                ">bIbIbIbI",
                # Valve 1
                1,
                4294967295,
                # Valve 2
                1,
                0,
                # Valve 3
                1,
                0,
                # Valve 4
                1,
                0,
            )
        )

        when(client_mock).read_gatt_char(MANUFACTURER_UUID).thenReturn(
            read_manufacturer
        )

        when(client_mock).read_gatt_char(BATTERY_UUID).thenReturn(read_battery)

        when(client_mock).read_gatt_char(VALVE_MANUAL_SETTINGS_UUID).thenReturn(
            read_manual_settings
        )

        when(client_mock).read_gatt_char(VALVE_MANUAL_STATES_UUID).thenReturn(
            read_manual_state
        )

        await device.connect()

        await device.fetch_state()

        verify(client_mock).read_gatt_char(VALVE_MANUAL_SETTINGS_UUID)

        assert device.battery_level == 30
        # assert device.brand == MELNOR

        assert device.zone1.is_watering is False
        assert device.zone1.manual_watering_minutes == 0
        # TODO: Test this more aggressively in date utils
        assert device.zone1.watering_end_time != 0

        assert device.zone2 is not None
        assert device.zone2.is_watering is False
        assert device.zone2.manual_watering_minutes == 0
        assert device.zone2.watering_end_time == 0

        assert device.zone3 is not None
        assert device.zone3.is_watering is False
        assert device.zone3.manual_watering_minutes == 0
        assert device.zone3.watering_end_time == 0

        assert device.zone4 is not None
        assert device.zone4.is_watering is False
        assert device.zone4.manual_watering_minutes == 0
        assert device.zone4.watering_end_time == 0

    def test_str(self, snapshot):
        device = Device(mac=TEST_UUID)

        actual = device.__str__()

        assert actual == snapshot
