"""Tests for the WLED select platform."""

from collections.abc import Generator
import typing
from unittest.mock import MagicMock, patch

from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from wled import (
    Device as WLEDDevice,
    Palette as WLEDPalette,
    Playlist as WLEDPlaylist,
    Preset as WLEDPreset,
    WLEDConnectionError,
    WLEDError,
)

from homeassistant.components.select import ATTR_OPTION, DOMAIN as SELECT_DOMAIN
from homeassistant.components.wled.const import DOMAIN, SCAN_INTERVAL
from homeassistant.const import (
    ATTR_ENTITY_ID,
    SERVICE_SELECT_OPTION,
    STATE_UNAVAILABLE,
    Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er

from tests.common import (
    MockConfigEntry,
    async_fire_time_changed,
    async_load_json_object_fixture,
    snapshot_platform,
)

pytestmark = pytest.mark.usefixtures("init_integration")


@pytest.fixture(autouse=True)
def override_platforms() -> Generator[None]:
    """Override PLATFORMS."""
    with patch("homeassistant.components.wled.PLATFORMS", [Platform.SELECT]):
        yield


async def test_snapshots(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    snapshot: SnapshotAssertion,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test snapshots of the platform."""
    await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)


@pytest.mark.parametrize(
    ("device_fixture", "entity_id", "option", "method", "called_with"),
    [
        (
            "rgb",
            "select.wled_rgb_light_segment_1_color_palette",
            "Icefire",
            "segment",
            {"segment_id": 1, "palette": "Icefire"},
        ),
        (
            "rgb",
            "select.wled_rgb_light_live_override",
            "2",
            "live",
            {"live": 2},
        ),
        (
            "rgbw",
            "select.wled_rgbw_light_playlist",
            "Playlist 2",
            "playlist",
            {"playlist": "Playlist 2"},
        ),
        (
            "rgbw",
            "select.wled_rgbw_light_preset",
            "Preset 2",
            "preset",
            {"preset": "Preset 2"},
        ),
    ],
)
async def test_color_palette_state(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    entity_registry: er.EntityRegistry,
    snapshot: SnapshotAssertion,
    mock_wled: MagicMock,
    entity_id: str,
    option: str,
    method: str,
    called_with: dict[str, int | str],
) -> None:
    """Test the behavior of the WLED selects."""
    method_mock = getattr(mock_wled, method)

    await hass.services.async_call(
        SELECT_DOMAIN,
        SERVICE_SELECT_OPTION,
        {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: option},
        blocking=True,
    )
    assert method_mock.call_count == 1
    method_mock.assert_called_with(**called_with)

    # Test invalid response, not becoming unavailable
    method_mock.side_effect = WLEDError
    with pytest.raises(HomeAssistantError, match="Invalid response from WLED API"):
        await hass.services.async_call(
            SELECT_DOMAIN,
            SERVICE_SELECT_OPTION,
            {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: option},
            blocking=True,
        )

    assert (state := hass.states.get(entity_id))
    assert state.state != STATE_UNAVAILABLE
    assert method_mock.call_count == 2
    method_mock.assert_called_with(**called_with)

    # Test connection error, leading to becoming unavailable
    method_mock.side_effect = WLEDConnectionError
    with pytest.raises(HomeAssistantError, match="Error communicating with WLED API"):
        await hass.services.async_call(
            SELECT_DOMAIN,
            SERVICE_SELECT_OPTION,
            {ATTR_ENTITY_ID: state.entity_id, ATTR_OPTION: option},
            blocking=True,
        )

    assert (state := hass.states.get(state.entity_id))
    assert state.state == STATE_UNAVAILABLE
    assert method_mock.call_count == 3
    method_mock.assert_called_with(**called_with)


@pytest.mark.parametrize("device_fixture", ["rgb_single_segment"])
async def test_color_palette_dynamically_handle_segments(
    hass: HomeAssistant,
    freezer: FrozenDateTimeFactory,
    mock_wled: MagicMock,
) -> None:
    """Test if a new/deleted segment is dynamically added/removed."""
    assert (segment0 := hass.states.get("select.wled_rgb_light_color_palette"))
    assert segment0.state == "Default"
    assert not hass.states.get("select.wled_rgb_light_segment_1_color_palette")

    return_value = mock_wled.update.return_value
    mock_wled.update.return_value = WLEDDevice.from_dict(
        await async_load_json_object_fixture(hass, "rgb.json", DOMAIN)
    )

    freezer.tick(SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert (segment0 := hass.states.get("select.wled_rgb_light_color_palette"))
    assert segment0.state == "Default"
    assert (
        segment1 := hass.states.get("select.wled_rgb_light_segment_1_color_palette")
    )
    assert segment1.state == "* Random Cycle"

    # Test adding if segment shows up again, including the master entity
    mock_wled.update.return_value = return_value
    freezer.tick(SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert (segment0 := hass.states.get("select.wled_rgb_light_color_palette"))
    assert segment0.state == "Default"
    assert (
        segment1 := hass.states.get("select.wled_rgb_light_segment_1_color_palette")
    )
    assert segment1.state == STATE_UNAVAILABLE


async def test_preset_unavailable_without_presets(hass: HomeAssistant) -> None:
    """Test WLED preset entity is unavailable when presets are not available."""
    assert (state := hass.states.get("select.wled_rgb_light_preset"))
    assert state.state == STATE_UNAVAILABLE


async def test_playlist_unavailable_without_playlists(hass: HomeAssistant) -> None:
    """Test WLED playlist entity is unavailable when playlists are not available."""
    assert (state := hass.states.get("select.wled_rgb_light_playlist"))
    assert state.state == STATE_UNAVAILABLE


PLAYLIST = {"ps": [1], "dur": [100], "transition": [7], "repeat": 0, "end": 0, "r": 0}


@pytest.mark.parametrize(
    ("entity_id", "data_attr", "new_data", "new_options"),
    [
        (
            "select.wled_rgb_light_preset",
            "presets",
            {
                1: WLEDPreset.from_dict({"preset_id": 1, "n": "Preset 1"}),
                2: WLEDPreset.from_dict({"preset_id": 2, "n": "Preset 2"}),
            },
            ["Preset 1", "Preset 2"],
        ),
        (
            "select.wled_rgb_light_playlist",
            "playlists",
            {
                1: WLEDPlaylist.from_dict(
                    {"playlist_id": 1, "n": "Playlist 1", "playlist": PLAYLIST}
                ),
                2: WLEDPlaylist.from_dict(
                    {"playlist_id": 2, "n": "Playlist 2", "playlist": PLAYLIST}
                ),
            },
            ["Playlist 1", "Playlist 2"],
        ),
        (
            "select.wled_rgb_light_color_palette",
            "palettes",
            {
                0: WLEDPalette.from_dict({"palette_id": 0, "name": "Palette 1"}),
                1: WLEDPalette.from_dict({"palette_id": 1, "name": "Palette 2"}),
            },
            ["Palette 1", "Palette 2"],
        ),
    ],
)
async def test_select_load_new_options_after_update(
    hass: HomeAssistant,
    freezer: FrozenDateTimeFactory,
    mock_wled: MagicMock,
    entity_id: str,
    data_attr: str,
    new_data: typing.Any,
    new_options: list[str],
) -> None:
    """Test WLED select entity is updated when new options are added."""
    setattr(
        mock_wled.update.return_value,
        data_attr,
        {},
    )

    freezer.tick(SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert (state := hass.states.get(entity_id))
    assert state.attributes["options"] == []

    setattr(
        mock_wled.update.return_value,
        data_attr,
        new_data,
    )

    freezer.tick(SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert (state := hass.states.get(entity_id))
    assert state.attributes["options"] == new_options
