"""Tests for the NeoPool light platform."""

from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock, patch

from freezegun.api import FrozenDateTimeFactory
from neopool_modbus import NeoPoolInvalidStateError
from neopool_modbus.exceptions import NeoPoolConnectionError
from neopool_modbus.registers import RelayKind, TimerRelayMode
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.neopool.const import FOLLOW_UP_REFRESH_DELAY
from homeassistant.const import (
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    STATE_OFF,
    STATE_ON,
    Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er

from . import setup_integration
from .conftest import MOCK_POOL_DATA

from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform


@pytest.fixture(autouse=True)
def _seed_light_relay_manual(mock_neopool_client: MagicMock) -> None:
    """Default the light relay timer to a manual mode so writes pass the guard."""
    mock_neopool_client.async_read_all.return_value = {
        **MOCK_POOL_DATA,
        "relay_light_enable": TimerRelayMode.ALWAYS_OFF,
        "Pool Light": False,
    }


async def _turn_on(hass: HomeAssistant, entity_id: str) -> None:
    await hass.services.async_call(
        LIGHT_DOMAIN,
        SERVICE_TURN_ON,
        {"entity_id": entity_id},
        blocking=True,
    )


async def _turn_off(hass: HomeAssistant, entity_id: str) -> None:
    await hass.services.async_call(
        LIGHT_DOMAIN,
        SERVICE_TURN_OFF,
        {"entity_id": entity_id},
        blocking=True,
    )


def _light_entity_id(hass: HomeAssistant, entry: MockConfigEntry) -> str:
    registry = er.async_get(hass)
    entries = [
        e
        for e in er.async_entries_for_config_entry(registry, entry.entry_id)
        if e.domain == LIGHT_DOMAIN
    ]
    assert len(entries) == 1, "expected exactly one neopool light entity"
    return entries[0].entity_id


async def test_light_turn_on_off_writes_via_relay_state(
    hass: HomeAssistant,
    mock_config_entry_light: MockConfigEntry,
    mock_neopool_client: MagicMock,
) -> None:
    """Light on/off delegates to the high-level async_set_relay_state API."""
    await setup_integration(hass, mock_config_entry_light)
    entity_id = _light_entity_id(hass, mock_config_entry_light)

    mock_neopool_client.async_set_relay_state = AsyncMock(
        return_value={"Pool Light": True}
    )
    await _turn_on(hass, entity_id)
    mock_neopool_client.async_set_relay_state.assert_awaited_once_with(
        RelayKind.LIGHT, True
    )
    assert hass.states.get(entity_id).state == STATE_ON

    mock_neopool_client.async_set_relay_state = AsyncMock(
        return_value={"Pool Light": False}
    )
    await _turn_off(hass, entity_id)
    mock_neopool_client.async_set_relay_state.assert_awaited_once_with(
        RelayKind.LIGHT, False
    )
    assert hass.states.get(entity_id).state == STATE_OFF


async def test_light_is_on_reflects_relay_state(
    hass: HomeAssistant,
    mock_config_entry_light: MockConfigEntry,
    mock_neopool_client: MagicMock,
    freezer: FrozenDateTimeFactory,
) -> None:
    """is_on tracks the "Pool Light" relay state key from a fresh poll."""
    await setup_integration(hass, mock_config_entry_light)
    entity_id = _light_entity_id(hass, mock_config_entry_light)

    mock_neopool_client.async_read_all.return_value = {
        **MOCK_POOL_DATA,
        "relay_light_enable": TimerRelayMode.ALWAYS_ON,
        "Pool Light": True,
    }
    freezer.tick(timedelta(seconds=60))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()
    assert hass.states.get(entity_id).state == STATE_ON

    mock_neopool_client.async_read_all.return_value = {
        **MOCK_POOL_DATA,
        "relay_light_enable": TimerRelayMode.ALWAYS_OFF,
        "Pool Light": False,
    }
    freezer.tick(timedelta(seconds=60))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()
    assert hass.states.get(entity_id).state == STATE_OFF


@pytest.mark.parametrize(
    "relay_data",
    [
        pytest.param({"relay_light_enable": TimerRelayMode.ENABLED}, id="auto"),
        pytest.param({}, id="missing"),
        pytest.param({"relay_light_enable": 0}, id="disabled"),
        pytest.param({"relay_light_enable": 2}, id="unknown-state"),
    ],
)
async def test_light_refuses_when_not_in_manual_mode(
    hass: HomeAssistant,
    mock_config_entry_light: MockConfigEntry,
    mock_neopool_client: MagicMock,
    freezer: FrozenDateTimeFactory,
    relay_data: dict[str, int],
) -> None:
    """Turn on/off is rejected while the relay is not in a manual mode."""
    await setup_integration(hass, mock_config_entry_light)
    entity_id = _light_entity_id(hass, mock_config_entry_light)

    mock_neopool_client.async_read_all.return_value = {**MOCK_POOL_DATA, **relay_data}
    freezer.tick(timedelta(seconds=60))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    mock_neopool_client.async_set_relay_state.reset_mock()
    with pytest.raises(ServiceValidationError):
        await _turn_on(hass, entity_id)
    with pytest.raises(ServiceValidationError):
        await _turn_off(hass, entity_id)
    mock_neopool_client.async_set_relay_state.assert_not_called()


async def test_light_maps_lib_invalid_state_to_service_validation(
    hass: HomeAssistant,
    mock_config_entry_light: MockConfigEntry,
    mock_neopool_client: MagicMock,
) -> None:
    """Lib-raised NeoPoolInvalidStateError is remapped to ServiceValidationError.

    Coordinator data may briefly lag the device state, so the pre-check passes
    but the library refuses. The mapping surfaces a translated error to users
    instead of leaking the raw library exception.
    """
    await setup_integration(hass, mock_config_entry_light)
    entity_id = _light_entity_id(hass, mock_config_entry_light)

    mock_neopool_client.async_set_relay_state = AsyncMock(
        side_effect=NeoPoolInvalidStateError("relay in auto mode")
    )
    with pytest.raises(ServiceValidationError):
        await _turn_on(hass, entity_id)
    mock_neopool_client.async_set_relay_state.assert_awaited_once_with(
        RelayKind.LIGHT, True
    )


@pytest.mark.parametrize(
    "write_error",
    [
        pytest.param(NeoPoolConnectionError("boom"), id="lib-connection-error"),
        pytest.param(TimeoutError("boom"), id="timeout"),
        pytest.param(OSError("boom"), id="os-error"),
    ],
)
async def test_light_maps_communication_error_to_home_assistant_error(
    hass: HomeAssistant,
    mock_config_entry_light: MockConfigEntry,
    mock_neopool_client: MagicMock,
    write_error: Exception,
) -> None:
    """Communication errors on write are surfaced as translated HomeAssistantError."""
    await setup_integration(hass, mock_config_entry_light)
    entity_id = _light_entity_id(hass, mock_config_entry_light)

    mock_neopool_client.async_set_relay_state = AsyncMock(side_effect=write_error)
    with pytest.raises(HomeAssistantError):
        await _turn_on(hass, entity_id)
    mock_neopool_client.async_set_relay_state.assert_awaited_once_with(
        RelayKind.LIGHT, True
    )


@pytest.mark.usefixtures("mock_neopool_client")
async def test_light_absent_when_option_off(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    mock_config_entry: MockConfigEntry,
) -> None:
    """No light entity is created while the use_light option is off."""
    await setup_integration(hass, mock_config_entry)
    light_entries = [
        e
        for e in er.async_entries_for_config_entry(
            entity_registry, mock_config_entry.entry_id
        )
        if e.domain == LIGHT_DOMAIN
    ]
    assert light_entries == []


async def test_light_absent_when_gpio_unassigned(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    mock_config_entry_light: MockConfigEntry,
    mock_neopool_client: MagicMock,
) -> None:
    """Light entity is not registered when the lighting GPIO is unassigned."""
    mock_neopool_client.async_read_all.return_value = {
        **MOCK_POOL_DATA,
        "MBF_PAR_LIGHTING_GPIO": 0,
        "relay_light_enable": TimerRelayMode.ALWAYS_OFF,
    }
    await setup_integration(hass, mock_config_entry_light)
    light_entries = [
        e
        for e in er.async_entries_for_config_entry(
            entity_registry, mock_config_entry_light.entry_id
        )
        if e.domain == LIGHT_DOMAIN
    ]
    assert light_entries == []


@pytest.mark.usefixtures("mock_neopool_client")
async def test_all_entities(
    hass: HomeAssistant,
    snapshot: SnapshotAssertion,
    entity_registry: er.EntityRegistry,
    mock_config_entry_light: MockConfigEntry,
) -> None:
    """Snapshot every light entity registered by the platform."""
    with patch("homeassistant.components.neopool.PLATFORMS", [Platform.LIGHT]):
        await setup_integration(hass, mock_config_entry_light)
    await snapshot_platform(
        hass, entity_registry, snapshot, mock_config_entry_light.entry_id
    )


async def test_light_write_schedules_follow_up_refresh(
    hass: HomeAssistant,
    mock_config_entry_light: MockConfigEntry,
    mock_neopool_client: MagicMock,
    freezer: FrozenDateTimeFactory,
) -> None:
    """A successful write triggers a second refresh after the follow-up delay."""
    await setup_integration(hass, mock_config_entry_light)
    entity_id = _light_entity_id(hass, mock_config_entry_light)

    mock_neopool_client.async_set_relay_state = AsyncMock(
        return_value={"Pool Light": True}
    )
    reads_before = mock_neopool_client.async_read_all.await_count
    await _turn_on(hass, entity_id)

    freezer.tick(timedelta(seconds=FOLLOW_UP_REFRESH_DELAY + 0.5))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert mock_neopool_client.async_read_all.await_count > reads_before


async def test_light_timer_enable_gates_writes(
    hass: HomeAssistant,
    mock_config_entry_light: MockConfigEntry,
    mock_neopool_client: MagicMock,
) -> None:
    """The relay-light timer enable read from read_all_timers gates writes.

    The enable field only reaches the write-guard via the timer read, so an
    auto mode here must make turn-on raise ServiceValidationError. A stale or
    missing read would leave the seeded ALWAYS_OFF and let the write through.
    """
    mock_neopool_client.read_all_timers = AsyncMock(
        return_value={
            "relay_light": {
                "enable": TimerRelayMode.ENABLED,
                "on": 3600,
                "interval": 7200,
                "period": 86400,
                "countdown": 120,
                "stop": 5400,
            }
        }
    )
    await setup_integration(hass, mock_config_entry_light)
    entity_id = _light_entity_id(hass, mock_config_entry_light)

    with pytest.raises(ServiceValidationError):
        await _turn_on(hass, entity_id)
    mock_neopool_client.async_set_relay_state.assert_not_called()
