"""Tests for iAquaLink integration."""

from unittest.mock import AsyncMock, patch

from freezegun.api import FrozenDateTimeFactory
import httpx
from iaqualink.client import AqualinkClient
from iaqualink.exception import (
    AqualinkServiceException,
    AqualinkServiceThrottledException,
    AqualinkServiceUnauthorizedException,
)
from iaqualink.systems.iaqua.device import IaquaLightSwitch
from iaqualink.systems.iaqua.system import IaquaSystem
import pytest

from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.iaqualink.const import UPDATE_INTERVAL_BY_SYSTEM_TYPE
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import (
    ATTR_ASSUMED_STATE,
    ATTR_ENTITY_ID,
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    STATE_OFF,
    STATE_ON,
    STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util

from .conftest import get_aqualink_device, get_aqualink_system, setup_integration

from tests.common import MockConfigEntry, async_fire_time_changed


async def _advance_coordinator_time(
    hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
    """Advance time to trigger coordinator update interval."""
    update_interval = UPDATE_INTERVAL_BY_SYSTEM_TYPE["iaqua"]

    freezer.tick(delta=update_interval)
    async_fire_time_changed(hass, dt_util.utcnow())
    await hass.async_block_till_done(wait_background_tasks=True)


@pytest.mark.parametrize(
    "raised_exception",
    [
        pytest.param(AqualinkServiceException, id="service"),
        pytest.param(TimeoutError, id="timeout"),
        pytest.param(httpx.HTTPError("boom"), id="http"),
    ],
)
async def test_system_refresh_failure_marks_entities_unavailable(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    client: AqualinkClient,
    freezer: FrozenDateTimeFactory,
    raised_exception: Exception | type[Exception],
) -> None:
    """Test a system refresh failure marks attached entities unavailable."""
    devices = await setup_integration(hass, config_entry, client)

    entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
    assert len(entity_ids) == 1
    entity_id = entity_ids[0]

    state = hass.states.get(entity_id)
    assert state is not None
    assert state.state == STATE_ON

    async def fail_update() -> None:
        devices.system.online = None
        raise raised_exception

    devices.system.update = AsyncMock(side_effect=fail_update)

    await _advance_coordinator_time(hass, freezer)

    state = hass.states.get(entity_id)
    assert state is not None
    assert state.state == STATE_UNAVAILABLE


async def test_system_rate_limited_keeps_entities_available(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    client: AqualinkClient,
    freezer: FrozenDateTimeFactory,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test a rate-limited update keeps entities at their last known state."""
    config_entry.add_to_hass(hass)

    system = get_aqualink_system(client, cls=IaquaSystem)
    system.online = True
    system.update = AsyncMock()
    systems = {system.serial: system}
    light = get_aqualink_device(
        system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"}
    )
    devices = {light.name: light}
    system.get_devices = AsyncMock(return_value=devices)

    with (
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.login",
            return_value=None,
        ),
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.get_systems",
            return_value=systems,
        ),
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
    assert len(entity_ids) == 1
    entity_id = entity_ids[0]

    state = hass.states.get(entity_id)
    assert state is not None
    assert state.state == STATE_ON

    system.update = AsyncMock(side_effect=AqualinkServiceThrottledException)

    await _advance_coordinator_time(hass, freezer)

    state = hass.states.get(entity_id)
    assert state is not None
    assert state.state == STATE_ON
    assert "Rate limited by iAquaLink" in caplog.text


async def test_light_service_calls_update_entity_state(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    client: AqualinkClient,
) -> None:
    """Test light service calls update entity state from device properties."""
    devices = await setup_integration(hass, config_entry, client)

    async def turn_off() -> None:
        devices.light.data["state"] = "0"

    async def turn_on() -> None:
        devices.light.data["state"] = "1"

    devices.light.turn_off = AsyncMock(side_effect=turn_off)
    devices.light.turn_on = AsyncMock(side_effect=turn_on)

    entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
    assert len(entity_ids) == 1
    entity_id = entity_ids[0]

    state = hass.states.get(entity_id)
    assert state is not None
    assert state.state == STATE_ON

    await hass.services.async_call(
        LIGHT_DOMAIN,
        SERVICE_TURN_OFF,
        {ATTR_ENTITY_ID: entity_id},
        blocking=True,
    )
    state = hass.states.get(entity_id)
    assert state is not None
    assert state.state == STATE_OFF

    await hass.services.async_call(
        LIGHT_DOMAIN,
        SERVICE_TURN_ON,
        {ATTR_ENTITY_ID: entity_id},
        blocking=True,
    )
    state = hass.states.get(entity_id)
    assert state is not None
    assert state.state == STATE_ON


@pytest.mark.parametrize(
    "raised_exception",
    [
        pytest.param(AqualinkServiceException, id="service"),
        pytest.param(TimeoutError, id="timeout"),
        pytest.param(httpx.HTTPError("boom"), id="http"),
    ],
)
async def test_setup_login_retry_exceptions(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    raised_exception: Exception | type[Exception],
) -> None:
    """Test setup retries on connection-related login exceptions."""
    config_entry.add_to_hass(hass)

    with patch(
        "homeassistant.components.iaqualink.AqualinkClient.login",
        side_effect=raised_exception,
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.SETUP_RETRY


async def test_setup_login_unauthorized(
    hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
    """Test setup encountering an unauthorized exception during login."""
    config_entry.add_to_hass(hass)

    with patch(
        "homeassistant.components.iaqualink.AqualinkClient.login",
        side_effect=AqualinkServiceUnauthorizedException,
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.SETUP_ERROR

    flows = hass.config_entries.flow.async_progress()
    assert len(flows) == 1
    assert flows[0]["context"]["source"] == SOURCE_REAUTH


@pytest.mark.parametrize(
    "raised_exception",
    [
        pytest.param(AqualinkServiceException, id="service"),
        pytest.param(TimeoutError, id="timeout"),
        pytest.param(httpx.HTTPError("boom"), id="http"),
    ],
)
async def test_setup_systems_exception(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    raised_exception: Exception | type[Exception],
) -> None:
    """Test setup encountering an exception while retrieving systems."""
    config_entry.add_to_hass(hass)

    with (
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.login",
            return_value=None,
        ),
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.get_systems",
            side_effect=raised_exception,
        ),
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.SETUP_RETRY


async def test_setup_systems_unauthorized(
    hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
    """Test setup encountering an unauthorized exception while retrieving systems."""
    config_entry.add_to_hass(hass)

    with (
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.login",
            return_value=None,
        ),
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.get_systems",
            side_effect=AqualinkServiceUnauthorizedException,
        ),
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.SETUP_ERROR

    flows = hass.config_entries.flow.async_progress()
    assert len(flows) == 1
    assert flows[0]["context"]["source"] == SOURCE_REAUTH


async def test_setup_first_refresh_unauthorized_closes_client(
    hass: HomeAssistant, config_entry: MockConfigEntry, client: AqualinkClient
) -> None:
    """Test setup closes the client when first refresh triggers reauthentication."""
    config_entry.add_to_hass(hass)

    system = get_aqualink_system(client, cls=IaquaSystem)
    system.update = AsyncMock(side_effect=AqualinkServiceUnauthorizedException)
    systems = {system.serial: system}

    with (
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.login",
            return_value=None,
        ),
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.get_systems",
            return_value=systems,
        ),
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.close",
            new_callable=AsyncMock,
        ) as mock_close,
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.SETUP_ERROR
    mock_close.assert_awaited_once()

    flows = hass.config_entries.flow.async_progress()
    assert len(flows) == 1
    assert flows[0]["context"]["source"] == SOURCE_REAUTH


async def test_setup_no_systems_recognized(
    hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
    """Test setup ending in no systems recognized."""
    config_entry.add_to_hass(hass)

    with (
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.login",
            return_value=None,
        ),
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.get_systems",
            return_value={},
        ),
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.SETUP_ERROR


@pytest.mark.parametrize(
    "raised_exception",
    [
        pytest.param(AqualinkServiceException, id="service"),
        pytest.param(TimeoutError, id="timeout"),
        pytest.param(httpx.HTTPError("boom"), id="http"),
    ],
)
async def test_setup_devices_exception(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    client: AqualinkClient,
    raised_exception: Exception | type[Exception],
) -> None:
    """Test setup encountering an exception while retrieving devices."""
    config_entry.add_to_hass(hass)

    system = get_aqualink_system(client, cls=IaquaSystem)
    system.online = True
    system.update = AsyncMock()
    systems = {system.serial: system}

    with (
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.login",
            return_value=None,
        ),
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.get_systems",
            return_value=systems,
        ),
        patch.object(
            system,
            "get_devices",
        ) as mock_get_devices,
    ):
        mock_get_devices.side_effect = raised_exception
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.SETUP_RETRY


async def test_setup_devices_unauthorized(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    client: AqualinkClient,
) -> None:
    """Test setup encountering an unauthorized exception while retrieving devices."""
    config_entry.add_to_hass(hass)

    system = get_aqualink_system(client, cls=IaquaSystem)
    system.online = True
    system.update = AsyncMock()
    systems = {system.serial: system}

    with (
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.login",
            return_value=None,
        ),
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.get_systems",
            return_value=systems,
        ),
        patch.object(
            system,
            "get_devices",
            side_effect=AqualinkServiceUnauthorizedException,
        ),
    ):
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.SETUP_ERROR

    flows = hass.config_entries.flow.async_progress()
    assert len(flows) == 1
    assert flows[0]["context"]["source"] == SOURCE_REAUTH


async def test_setup_all_good_no_recognized_devices(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    client: AqualinkClient,
) -> None:
    """Test setup ending in no devices recognized."""
    config_entry.add_to_hass(hass)

    system = get_aqualink_system(client, cls=IaquaSystem)
    system.online = True
    system.update = AsyncMock()
    systems = {system.serial: system}

    device = get_aqualink_device(system, name="dev_1")
    devices = {device.name: device}

    with (
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.login",
            return_value=None,
        ),
        patch(
            "homeassistant.components.iaqualink.AqualinkClient.get_systems",
            return_value=systems,
        ),
        patch.object(
            system,
            "get_devices",
        ) as mock_get_devices,
    ):
        mock_get_devices.return_value = devices
        await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.LOADED

    assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 0
    assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 0
    assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 0
    assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0
    assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0

    assert await hass.config_entries.async_unload(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.NOT_LOADED


async def test_setup_all_good_all_device_types(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    client: AqualinkClient,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test setup ending in one device of each type recognized."""
    await setup_integration(hass, config_entry, client)

    assert config_entry.state is ConfigEntryState.LOADED

    assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1
    assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 1
    assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1
    assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1
    assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1

    for domain in (
        BINARY_SENSOR_DOMAIN,
        CLIMATE_DOMAIN,
        LIGHT_DOMAIN,
        SENSOR_DOMAIN,
        SWITCH_DOMAIN,
    ):
        for entity_id in hass.states.async_entity_ids(domain):
            entry = entity_registry.async_get(entity_id)
            assert entry is not None
            assert entry.has_entity_name is True

    assert await hass.config_entries.async_unload(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.NOT_LOADED


async def test_multiple_updates(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    client: AqualinkClient,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test all possible results of online status transition after update."""
    devices = await setup_integration(hass, config_entry, client)
    system = devices.system

    assert config_entry.state is ConfigEntryState.LOADED

    entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
    assert len(entity_ids) == 1
    entity_id = entity_ids[0]

    def assert_state(expected_state: str) -> None:
        state = hass.states.get(entity_id)
        assert state is not None
        assert state.state == expected_state

    def set_online_to_true():
        system.online = True

    def set_online_to_false():
        system.online = False

    async def fail_update() -> None:
        system.online = None
        raise AqualinkServiceException

    system.update = AsyncMock()

    # True -> True
    system.online = True
    system.update.side_effect = set_online_to_true
    await _advance_coordinator_time(hass, freezer)
    assert system.update.await_count == 1
    assert_state(STATE_ON)

    # True -> False
    system.online = True
    system.update.side_effect = set_online_to_false
    await _advance_coordinator_time(hass, freezer)
    assert system.update.await_count == 2
    assert_state(STATE_UNAVAILABLE)

    # True -> None / ServiceException
    system.online = True
    system.update.side_effect = fail_update
    await _advance_coordinator_time(hass, freezer)
    assert system.update.await_count == 3
    assert_state(STATE_UNAVAILABLE)

    # False -> False
    system.online = False
    system.update.side_effect = set_online_to_false
    await _advance_coordinator_time(hass, freezer)
    assert system.update.await_count == 4
    assert_state(STATE_UNAVAILABLE)

    # False -> True
    system.online = False
    system.update.side_effect = set_online_to_true
    await _advance_coordinator_time(hass, freezer)
    assert system.update.await_count == 5
    assert_state(STATE_ON)

    # False -> None / ServiceException
    system.online = False
    system.update.side_effect = fail_update
    await _advance_coordinator_time(hass, freezer)
    assert system.update.await_count == 6
    assert_state(STATE_UNAVAILABLE)

    # None -> None / ServiceException
    system.online = None
    system.update.side_effect = fail_update
    await _advance_coordinator_time(hass, freezer)
    assert system.update.await_count == 7
    assert_state(STATE_UNAVAILABLE)

    # None -> True
    system.online = None
    system.update.side_effect = set_online_to_true
    await _advance_coordinator_time(hass, freezer)
    assert system.update.await_count == 8
    assert_state(STATE_ON)

    # None -> False
    system.online = None
    system.update.side_effect = set_online_to_false
    await _advance_coordinator_time(hass, freezer)
    assert system.update.await_count == 9
    assert_state(STATE_UNAVAILABLE)

    assert await hass.config_entries.async_unload(config_entry.entry_id)
    await hass.async_block_till_done()

    assert config_entry.state is ConfigEntryState.NOT_LOADED


async def test_entity_assumed_and_available(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    client: AqualinkClient,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test assumed_state and_available properties for all values of online."""
    devices = await setup_integration(hass, config_entry, client)

    entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
    assert len(entity_ids) == 1

    name = entity_ids[0]

    # None means maybe.
    devices.light.system.online = None
    await _advance_coordinator_time(hass, freezer)
    state = hass.states.get(name)
    assert state.state == STATE_UNAVAILABLE
    assert state.attributes.get(ATTR_ASSUMED_STATE) is True

    devices.light.system.online = False
    await _advance_coordinator_time(hass, freezer)
    state = hass.states.get(name)
    assert state.state == STATE_UNAVAILABLE
    assert state.attributes.get(ATTR_ASSUMED_STATE) is True

    devices.light.system.online = True
    await _advance_coordinator_time(hass, freezer)
    state = hass.states.get(name)
    assert state.state == STATE_ON
    assert state.attributes.get(ATTR_ASSUMED_STATE) is None


async def test_system_refresh_unauthorized_triggers_reauth(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    client: AqualinkClient,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test an unauthorized refresh starts reauthentication."""
    devices = await setup_integration(hass, config_entry, client)

    assert config_entry.state is ConfigEntryState.LOADED

    devices.system.update = AsyncMock(side_effect=AqualinkServiceUnauthorizedException)

    await _advance_coordinator_time(hass, freezer)

    flows = hass.config_entries.flow.async_progress()
    assert len(flows) == 1
    assert flows[0]["context"]["source"] == SOURCE_REAUTH
    assert flows[0]["context"]["entry_id"] == config_entry.entry_id
