from __future__ import annotations

import time
import unittest
from unittest.mock import MagicMock, patch

import pytest

from iaqualink.client import AqualinkClient
from iaqualink.exception import (
    AqualinkServiceException,
    AqualinkServiceUnauthorizedException,
    AqualinkSystemOfflineException,
)
from iaqualink.system import AqualinkSystem
from iaqualink.systems.exo.device import (
    ExoAttributeSwitch,
    ExoAuxSwitch,
    ExoSensor,
)
from iaqualink.systems.exo.system import ExoSystem

from ...common import async_noop, async_raises

SAMPLE_DATA = {
    "state": {
        "reported": {
            "vr": "V85W4",
            "aws": {
                "status": "connected",
                "timestamp": 123,
                "session_id": "xxxx",
            },
            "hmi": {
                "ff": {
                    "fn": "/fluidra-ota-prod/exo/V85W4_OTA.bin",
                    "vr": "V85W4",
                    "ts": 123,
                    "pg": {"fs": 507300, "bd": 507300, "ts": 123, "te": 123},
                },
                "fw": {
                    "fn": "/fluidra-ota-prod/exo/V85W4_OTA.bin",
                    "vr": "V85W4",
                },
            },
            "main": {
                "ff": {
                    "fn": "/fluidra-ota-prod/exo/V85R67_OTA.bin",
                    "vr": "V85R67",
                    "ts": 123,
                    "pg": {"fs": 402328, "bd": 402328, "ts": 123, "te": 123},
                }
            },
            "debug": {
                "RSSI": -26,
                "OTA fail": 1,
                "OTA State": 0,
                "Last error": 65278,
                "Still alive": 2,
                "OTA success": 9,
                "MQTT connection": 2,
                "OTA fail global": 0,
                "Version Firmware": "V85W4B0",
                "Nb_Success_Pub_MSG": 463,
                "Nb_Fail_Publish_MSG": 0,
                "Nb_Success_Sub_Receive": 2,
                "MQTT disconnection total": 1,
                "OTA fail by disconnection": 0,
                "Nb reboot du to MQTT issue": 669,
            },
            "state": {"reported": {"debug_main": {"tr": 100}}},
            "equipment": {
                "swc_0": {
                    "vr": "V85R67",
                    "sn": "xxxxx",
                    "amp": 1,
                    "vsp": 1,
                    "low": 0,
                    "swc": 50,
                    "temp": 1,
                    "lang": 2,
                    "ph_sp": 74,
                    "sns_1": {"state": 1, "value": 75, "sensor_type": "Ph"},
                    "aux_1": {
                        "type": "none",
                        "mode": 0,
                        "color": 0,
                        "state": 0,
                    },
                    "sns_2": {"state": 1, "value": 780, "sensor_type": "Orp"},
                    "sns_3": {
                        "state": 1,
                        "value": 29,
                        "sensor_type": "Water temp",
                    },
                    "aux_2": {
                        "type": "none",
                        "mode": 0,
                        "state": 0,
                        "color": 0,
                    },
                    "boost": 0,
                    "orp_sp": 830,
                    "aux230": 1,
                    "ph_only": 1,
                    "swc_low": 0,
                    "version": "V1",
                    "exo_state": 1,
                    "dual_link": 1,
                    "production": 1,
                    "error_code": 0,
                    "boost_time": "24:00",
                    "filter_pump": {"type": 1, "state": 1},
                    "error_state": 0,
                }
            },
            "schedules": {
                "sch9": {
                    "id": "sch_9",
                    "name": "Aux 1",
                    "timer": {"end": "00:00", "start": "00:00"},
                    "active": 0,
                    "enabled": 0,
                    "endpoint": "aux1",
                },
                "sch1": {
                    "id": "sch_1",
                    "name": "Salt Water Chlorinator 1",
                    "timer": {"end": "00:00", "start": "00:00"},
                    "active": 0,
                    "enabled": 0,
                    "endpoint": "swc_1",
                },
                "sch3": {
                    "id": "sch_3",
                    "name": "Filter Pump 1",
                    "timer": {"end": "00:00", "start": "00:00"},
                    "active": 0,
                    "enabled": 0,
                    "endpoint": "ssp_1",
                },
                "sch4": {
                    "id": "sch_4",
                    "name": "Filter Pump 2",
                    "timer": {"end": "00:00", "start": "00:00"},
                    "active": 0,
                    "enabled": 0,
                    "endpoint": "ssp_2",
                },
                "sch2": {
                    "id": "sch_2",
                    "name": "Salt Water Chlorinator 2",
                    "timer": {"end": "00:00", "start": "00:00"},
                    "active": 0,
                    "enabled": 0,
                    "endpoint": "swc_2",
                },
                "sch10": {
                    "id": "sch_10",
                    "name": "Aux 2",
                    "timer": {"end": "00:00", "start": "00:00"},
                    "active": 0,
                    "enabled": 0,
                    "endpoint": "aux2",
                },
                "supported": 6,
                "programmed": 0,
            },
        }
    },
    "deviceId": "123",
    "ts": 123,
}


class TestExoSystem(unittest.IsolatedAsyncioTestCase):
    def setUp(self) -> None:
        pass

    def test_from_data_iaqua(self):
        aqualink = MagicMock()
        data = {"id": 1, "serial_number": "ABCDEFG", "device_type": "exo"}
        r = AqualinkSystem.from_data(aqualink, data)
        assert r is not None
        assert isinstance(r, ExoSystem)

    async def test_update_success(self):
        aqualink = MagicMock()
        data = {"id": 1, "serial_number": "ABCDEFG", "device_type": "exo"}
        r = AqualinkSystem.from_data(aqualink, data)
        r.send_reported_state_request = async_noop
        r._parse_shadow_response = MagicMock()
        await r.update()
        assert r.online is True

    async def test_update_service_exception(self):
        aqualink = MagicMock()
        data = {"id": 1, "serial_number": "ABCDEFG", "device_type": "exo"}
        r = AqualinkSystem.from_data(aqualink, data)
        r.send_reported_state_request = async_raises(AqualinkServiceException)
        with pytest.raises(AqualinkServiceException):
            await r.update()
        assert r.online is None

    async def test_update_offline(self):
        aqualink = MagicMock()
        data = {"id": 1, "serial_number": "ABCDEFG", "device_type": "exo"}
        r = AqualinkSystem.from_data(aqualink, data)
        r.send_reported_state_request = async_noop
        r._send_devices_screen_request = async_noop
        r._parse_shadow_response = MagicMock(
            side_effect=AqualinkSystemOfflineException
        )

        with pytest.raises(AqualinkSystemOfflineException):
            await r.update()
        assert r.online is False

    def test_parse_devices_good(self):
        data = {"id": 1, "serial_number": "ABCDEFG", "device_type": "exo"}
        aqualink = MagicMock()
        system = ExoSystem.from_data(aqualink, data)

        response = MagicMock()
        response.json.return_value = SAMPLE_DATA
        system._parse_shadow_response(response)

        assert len(system.devices) > 0
        # Chemistry sensors
        assert "sns_1" in system.devices
        assert isinstance(system.devices["sns_1"], ExoSensor)
        assert "sns_2" in system.devices
        assert isinstance(system.devices["sns_2"], ExoSensor)
        assert "sns_3" in system.devices
        assert isinstance(system.devices["sns_3"], ExoSensor)
        # Auxiliary switches
        assert "aux_1" in system.devices
        assert isinstance(system.devices["aux_1"], ExoAuxSwitch)
        assert "aux_2" in system.devices
        assert isinstance(system.devices["aux_2"], ExoAuxSwitch)
        # Attribute switches
        assert "boost" in system.devices
        assert isinstance(system.devices["boost"], ExoAttributeSwitch)
        assert "production" in system.devices
        assert isinstance(system.devices["production"], ExoAttributeSwitch)
        # Filter pump
        assert "filter_pump" in system.devices
        # Excluded keys must be absent
        assert "sn" not in system.devices
        assert "vr" not in system.devices
        assert "version" not in system.devices
        assert "boost_time" not in system.devices

    @patch("httpx.AsyncClient.request")
    async def test_reported_state_request(self, mock_request):
        data = {"id": 1, "serial_number": "ABCDEFG", "device_type": "exo"}
        aqualink = AqualinkClient("user", "pass")
        system = ExoSystem.from_data(aqualink, data)

        mock_request.return_value.status_code = 200

        await system.send_reported_state_request()

    @patch("httpx.AsyncClient.request")
    async def test_reported_state_request_unauthorized(self, mock_request):
        data = {"id": 1, "serial_number": "ABCDEFG", "device_type": "exo"}
        aqualink = AqualinkClient("user", "pass")
        system = ExoSystem.from_data(aqualink, data)

        mock_request.return_value.status_code = 401

        with pytest.raises(AqualinkServiceUnauthorizedException):
            await system.send_reported_state_request()

    @patch("httpx.AsyncClient.request")
    async def test_reported_state_request_retries_after_refresh(
        self, mock_request
    ):
        data = {"id": 1, "serial_number": "ABCDEFG", "device_type": "exo"}
        aqualink = AqualinkClient("user", "pass")
        system = ExoSystem.from_data(aqualink, data)

        mock_request.side_effect = [
            MagicMock(status_code=401),
            MagicMock(status_code=200),
        ]
        aqualink.id_token = "old-id-token"

        async def fake_refresh() -> None:
            aqualink.id_token = "new-id-token"

        with patch.object(
            aqualink, "_refresh_auth", side_effect=fake_refresh
        ) as mock_refresh:
            await system.send_reported_state_request()

        retry_headers = mock_request.call_args_list[1][1]["headers"]

        mock_refresh.assert_awaited_once()
        assert retry_headers["Authorization"] == "new-id-token"

    @patch("httpx.AsyncClient.request")
    async def test_reported_state_request_refreshes_only_once_on_repeated_401(
        self, mock_request
    ):
        data = {"id": 1, "serial_number": "ABCDEFG", "device_type": "exo"}
        aqualink = AqualinkClient("user", "pass")
        system = ExoSystem.from_data(aqualink, data)

        mock_request.side_effect = [
            MagicMock(status_code=401),
            MagicMock(status_code=401),
        ]

        with (
            patch.object(
                aqualink, "_refresh_auth", return_value=None
            ) as mock_refresh,
            pytest.raises(AqualinkServiceUnauthorizedException),
        ):
            await system.send_reported_state_request()

        mock_refresh.assert_awaited_once()

    async def test_update_skipped_within_refresh_interval(self):
        aqualink = MagicMock()
        data = {"id": 1, "serial_number": "ABCDEFG", "device_type": "exo"}
        system = AqualinkSystem.from_data(aqualink, data)
        system.send_reported_state_request = async_noop
        system._parse_shadow_response = MagicMock()

        # First update should go through.
        now = int(time.time())
        with patch("iaqualink.systems.exo.system.time") as mock_time:
            mock_time.time.return_value = now
            await system.update()
        assert system._parse_shadow_response.call_count == 1

        # Second update within MIN_SECS_TO_REFRESH should be skipped.
        system._parse_shadow_response.reset_mock()
        with patch("iaqualink.systems.exo.system.time") as mock_time:
            mock_time.time.return_value = (
                now + ExoSystem.MIN_SECS_TO_REFRESH - 1
            )
            await system.update()
        assert system._parse_shadow_response.call_count == 0

        # Update after MIN_SECS_TO_REFRESH should go through.
        with patch("iaqualink.systems.exo.system.time") as mock_time:
            mock_time.time.return_value = now + ExoSystem.MIN_SECS_TO_REFRESH
            await system.update()
        assert system._parse_shadow_response.call_count == 1
