"""Test Matter Fan platform."""

from unittest.mock import MagicMock, call

from matter_server.client.models.node import MatterNode
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.fan import (
    ATTR_DIRECTION,
    ATTR_OSCILLATING,
    ATTR_PERCENTAGE,
    ATTR_PRESET_MODE,
    DIRECTION_FORWARD,
    DIRECTION_REVERSE,
    DOMAIN as FAN_DOMAIN,
    SERVICE_OSCILLATE,
    SERVICE_SET_DIRECTION,
    SERVICE_SET_PERCENTAGE,
    FanEntityFeature,
)
from homeassistant.const import (
    ATTR_ENTITY_ID,
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er

from .common import (
    set_node_attribute,
    snapshot_matter_entities,
    trigger_subscription_callback,
)


@pytest.mark.usefixtures("matter_devices")
async def test_fans(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    snapshot: SnapshotAssertion,
) -> None:
    """Test fans."""
    snapshot_matter_entities(hass, entity_registry, snapshot, Platform.FAN)


@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_fan_base(
    hass: HomeAssistant,
    matter_client: MagicMock,
    matter_node: MatterNode,
) -> None:
    """Test Fan platform."""
    entity_id = "fan.mock_air_purifier"
    state = hass.states.get(entity_id)
    assert state
    assert state.attributes["preset_modes"] == [
        "low",
        "medium",
        "high",
        "auto",
        "natural_wind",
        "sleep_wind",
    ]
    assert state.attributes["direction"] == "forward"
    assert state.attributes["oscillating"] is False
    assert state.attributes["percentage"] is None
    assert state.attributes["percentage_step"] == 10
    assert state.attributes["preset_mode"] == "auto"
    mask = (
        FanEntityFeature.DIRECTION
        | FanEntityFeature.OSCILLATE
        | FanEntityFeature.PRESET_MODE
        | FanEntityFeature.SET_SPEED
    )
    assert state.attributes["supported_features"] & mask == mask
    # handle fan mode update
    set_node_attribute(matter_node, 1, 514, 0, 1)
    await trigger_subscription_callback(hass, matter_client)
    state = hass.states.get(entity_id)
    assert state.attributes["preset_mode"] == "low"
    # handle direction update
    set_node_attribute(matter_node, 1, 514, 11, 1)
    await trigger_subscription_callback(hass, matter_client)
    state = hass.states.get(entity_id)
    assert state.attributes["direction"] == "reverse"
    # handle rock/oscillation update
    set_node_attribute(matter_node, 1, 514, 8, 1)
    await trigger_subscription_callback(hass, matter_client)
    state = hass.states.get(entity_id)
    assert state.attributes["oscillating"] is True
    # handle wind mode active translates to correct preset
    set_node_attribute(matter_node, 1, 514, 10, 2)
    await trigger_subscription_callback(hass, matter_client)
    state = hass.states.get(entity_id)
    assert state.attributes["preset_mode"] == "natural_wind"
    set_node_attribute(matter_node, 1, 514, 10, 1)
    await trigger_subscription_callback(hass, matter_client)
    state = hass.states.get(entity_id)
    assert state.attributes["preset_mode"] == "sleep_wind"
    # set mains power to OFF (OnOff cluster)
    set_node_attribute(matter_node, 1, 6, 0, False)
    await trigger_subscription_callback(hass, matter_client)
    state = hass.states.get(entity_id)
    assert state.attributes["preset_mode"] is None
    assert state.attributes["percentage"] == 0
    # test featuremap update
    set_node_attribute(matter_node, 1, 514, 65532, 1)
    await trigger_subscription_callback(hass, matter_client)
    state = hass.states.get(entity_id)
    assert state.attributes["supported_features"] & FanEntityFeature.SET_SPEED


@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_fan_turn_on_with_percentage(
    hass: HomeAssistant,
    matter_client: MagicMock,
    matter_node: MatterNode,
) -> None:
    """Test turning on the fan with a specific percentage."""
    entity_id = "fan.mock_air_purifier"
    await hass.services.async_call(
        FAN_DOMAIN,
        SERVICE_TURN_ON,
        {ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 50},
        blocking=True,
    )
    assert matter_client.write_attribute.call_count == 1
    assert matter_client.write_attribute.call_args == call(
        node_id=matter_node.node_id,
        attribute_path="1/514/2",
        value=50,
    )
    # test again where preset_mode is omitted in the service call
    # which should select the last active percentage
    matter_client.write_attribute.reset_mock()
    await hass.services.async_call(
        FAN_DOMAIN,
        SERVICE_TURN_ON,
        {ATTR_ENTITY_ID: entity_id},
        blocking=True,
    )
    assert matter_client.write_attribute.call_count == 1
    assert matter_client.write_attribute.call_args == call(
        node_id=matter_node.node_id,
        attribute_path="1/514/2",
        value=255,
    )


@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("node_fixture", ["mock_fan"])
async def test_fan_turn_on_with_preset_mode(
    hass: HomeAssistant,
    matter_client: MagicMock,
    matter_node: MatterNode,
) -> None:
    """Test turning on the fan with a specific preset mode."""
    entity_id = "fan.mocked_fan_switch"
    await hass.services.async_call(
        FAN_DOMAIN,
        SERVICE_TURN_ON,
        {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "medium"},
        blocking=True,
    )
    assert matter_client.write_attribute.call_count == 1
    assert matter_client.write_attribute.call_args == call(
        node_id=matter_node.node_id,
        attribute_path="1/514/0",
        value=2,
    )
    # test again with wind feature as preset mode
    for preset_mode, value in (("natural_wind", 2), ("sleep_wind", 1)):
        matter_client.write_attribute.reset_mock()
        await hass.services.async_call(
            FAN_DOMAIN,
            SERVICE_TURN_ON,
            {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset_mode},
            blocking=True,
        )
        assert matter_client.write_attribute.call_count == 1
        assert matter_client.write_attribute.call_args == call(
            node_id=matter_node.node_id,
            attribute_path="1/514/10",
            value=value,
        )
    # test again if wind mode is explicitly turned off when we set a new preset mode
    matter_client.write_attribute.reset_mock()
    set_node_attribute(matter_node, 1, 514, 10, 2)
    await trigger_subscription_callback(hass, matter_client)
    await hass.services.async_call(
        FAN_DOMAIN,
        SERVICE_TURN_ON,
        {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "medium"},
        blocking=True,
    )
    assert matter_client.write_attribute.call_count == 2
    assert matter_client.write_attribute.call_args_list[0] == call(
        node_id=matter_node.node_id,
        attribute_path="1/514/10",
        value=0,
    )
    assert matter_client.write_attribute.call_args == call(
        node_id=matter_node.node_id,
        attribute_path="1/514/0",
        value=2,
    )
    # test again where preset_mode is omitted in the service call
    # which should select the last active preset
    matter_client.write_attribute.reset_mock()
    set_node_attribute(matter_node, 1, 514, 0, 1)
    set_node_attribute(matter_node, 1, 514, 10, 0)
    await trigger_subscription_callback(hass, matter_client)
    await hass.services.async_call(
        FAN_DOMAIN,
        SERVICE_TURN_ON,
        {ATTR_ENTITY_ID: entity_id},
        blocking=True,
    )
    assert matter_client.write_attribute.call_count == 1
    assert matter_client.write_attribute.call_args == call(
        node_id=matter_node.node_id,
        attribute_path="1/514/0",
        value=1,
    )


@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_fan_turn_off(
    hass: HomeAssistant,
    matter_client: MagicMock,
    matter_node: MatterNode,
) -> None:
    """Test turning off the fan."""
    entity_id = "fan.mock_air_purifier"
    await hass.services.async_call(
        FAN_DOMAIN,
        SERVICE_TURN_OFF,
        {ATTR_ENTITY_ID: entity_id},
        blocking=True,
    )
    assert matter_client.write_attribute.call_count == 1
    assert matter_client.write_attribute.call_args == call(
        node_id=matter_node.node_id,
        attribute_path="1/514/0",
        value=0,
    )
    matter_client.write_attribute.reset_mock()
    # test again if wind mode is turned off
    set_node_attribute(matter_node, 1, 514, 10, 2)
    await trigger_subscription_callback(hass, matter_client)
    await hass.services.async_call(
        FAN_DOMAIN,
        SERVICE_TURN_OFF,
        {ATTR_ENTITY_ID: entity_id},
        blocking=True,
    )
    assert matter_client.write_attribute.call_count == 2
    assert matter_client.write_attribute.call_args_list[0] == call(
        node_id=matter_node.node_id,
        attribute_path="1/514/10",
        value=0,
    )
    assert matter_client.write_attribute.call_args_list[1] == call(
        node_id=matter_node.node_id,
        attribute_path="1/514/0",
        value=0,
    )


@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_fan_oscillate(
    hass: HomeAssistant,
    matter_client: MagicMock,
    matter_node: MatterNode,
) -> None:
    """Test oscillating the fan."""
    entity_id = "fan.mock_air_purifier"
    for oscillating, value in ((True, 1), (False, 0)):
        await hass.services.async_call(
            FAN_DOMAIN,
            SERVICE_OSCILLATE,
            {ATTR_ENTITY_ID: entity_id, ATTR_OSCILLATING: oscillating},
            blocking=True,
        )
        assert matter_client.write_attribute.call_count == 1
        assert matter_client.write_attribute.call_args == call(
            node_id=matter_node.node_id,
            attribute_path="1/514/8",
            value=value,
        )
        matter_client.write_attribute.reset_mock()


@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
async def test_fan_set_direction(
    hass: HomeAssistant,
    matter_client: MagicMock,
    matter_node: MatterNode,
) -> None:
    """Test oscillating the fan."""
    entity_id = "fan.mock_air_purifier"
    for direction, value in ((DIRECTION_FORWARD, 0), (DIRECTION_REVERSE, 1)):
        await hass.services.async_call(
            FAN_DOMAIN,
            SERVICE_SET_DIRECTION,
            {ATTR_ENTITY_ID: entity_id, ATTR_DIRECTION: direction},
            blocking=True,
        )
        assert matter_client.write_attribute.call_count == 1
        assert matter_client.write_attribute.call_args == call(
            node_id=matter_node.node_id,
            attribute_path="1/514/11",
            value=value,
        )
        matter_client.write_attribute.reset_mock()


@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize(
    ("node_fixture", "entity_id", "attributes", "features"),
    [
        (
            "mock_fan",
            "fan.mocked_fan_switch",
            {
                "1/514/65532": 0,
            },
            (FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF),
        ),
        (
            "mock_fan",
            "fan.mocked_fan_switch",
            {
                "1/514/65532": 1,
            },
            (
                FanEntityFeature.TURN_ON
                | FanEntityFeature.TURN_OFF
                | FanEntityFeature.SET_SPEED
            ),
        ),
        (
            "mock_fan",
            "fan.mocked_fan_switch",
            {
                "1/514/65532": 4,
            },
            (
                FanEntityFeature.TURN_ON
                | FanEntityFeature.TURN_OFF
                | FanEntityFeature.OSCILLATE
            ),
        ),
        (
            "mock_fan",
            "fan.mocked_fan_switch",
            {
                "1/514/65532": 36,
            },
            (
                FanEntityFeature.TURN_ON
                | FanEntityFeature.TURN_OFF
                | FanEntityFeature.OSCILLATE
                | FanEntityFeature.DIRECTION
            ),
        ),
    ],
)
async def test_fan_supported_features(
    hass: HomeAssistant,
    matter_client: MagicMock,
    matter_node: MatterNode,
    entity_id: str,
    features: int,
) -> None:
    """Test if the correct features get discovered from featuremap."""
    state = hass.states.get(entity_id)
    assert state
    assert state.attributes["supported_features"] & features == features


@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize(
    ("node_fixture", "entity_id", "attributes", "preset_modes"),
    [
        (
            "mock_fan",
            "fan.mocked_fan_switch",
            {"1/514/1": 0, "1/514/65532": 0},
            [
                "low",
                "medium",
                "high",
            ],
        ),
        (
            "mock_fan",
            "fan.mocked_fan_switch",
            {"1/514/1": 1, "1/514/65532": 0},
            [
                "low",
                "high",
            ],
        ),
        (
            "mock_fan",
            "fan.mocked_fan_switch",
            {"1/514/1": 2, "1/514/65532": 0},
            ["low", "medium", "high", "auto"],
        ),
        (
            "mock_fan",
            "fan.mocked_fan_switch",
            {"1/514/1": 4, "1/514/65532": 0},
            ["high", "auto"],
        ),
        (
            "mock_fan",
            "fan.mocked_fan_switch",
            {"1/514/1": 5, "1/514/65532": 0},
            ["high"],
        ),
        (
            "mock_fan",
            "fan.mocked_fan_switch",
            {"1/514/1": 5, "1/514/65532": 8, "1/514/9": 3},
            ["high", "natural_wind", "sleep_wind"],
        ),
    ],
)
async def test_fan_features(
    hass: HomeAssistant,
    matter_client: MagicMock,
    matter_node: MatterNode,
    entity_id: str,
    preset_modes: list[str],
) -> None:
    """Test if the correct presets get discovered from fanmodesequence."""
    state = hass.states.get(entity_id)
    assert state
    assert state.attributes["preset_modes"] == preset_modes


@pytest.mark.parametrize("node_fixture", ["silabs_range_hood"])
async def test_fan_set_percentage_without_multispeed(
    hass: HomeAssistant,
    matter_client: MagicMock,
    matter_node: MatterNode,
) -> None:
    """Test percentage control on a fan without the MultiSpeed feature.

    PercentSetting is mandatory in the FanControl cluster regardless of features,
    so SET_SPEED must be available and write to PercentSetting (attribute 0x0002).
    """
    entity_id = "fan.sl_rangehood"
    state = hass.states.get(entity_id)
    assert state
    assert state.attributes["supported_features"] & FanEntityFeature.SET_SPEED
    assert state.attributes["percentage_step"] == 1.0

    await hass.services.async_call(
        FAN_DOMAIN,
        SERVICE_SET_PERCENTAGE,
        {ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 75},
        blocking=True,
    )
    assert matter_client.write_attribute.call_count == 1
    assert matter_client.write_attribute.call_args == call(
        node_id=matter_node.node_id,
        attribute_path="1/514/2",
        value=75,
    )
