"""Package to test the get_accessory method."""

from unittest.mock import Mock, patch

import pytest

from homeassistant.components.climate import ClimateEntityFeature
from homeassistant.components.cover import CoverEntityFeature
from homeassistant.components.homekit import TYPE_AIR_PURIFIER
from homeassistant.components.homekit.accessories import TYPES, get_accessory
from homeassistant.components.homekit.const import (
    ATTR_INTEGRATION,
    CONF_FEATURE_LIST,
    FEATURE_ON_OFF,
    TYPE_FAN,
    TYPE_FAUCET,
    TYPE_OUTLET,
    TYPE_SHOWER,
    TYPE_SPRINKLER,
    TYPE_SWITCH,
    TYPE_VALVE,
)
from homeassistant.components.homekit.type_sensors import (
    AirQualitySensor,
    CarbonDioxideSensor,
    PM10Sensor,
    PM25Sensor,
    TemperatureSensor,
)
from homeassistant.components.media_player import (
    MediaPlayerDeviceClass,
    MediaPlayerEntityFeature,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.components.vacuum import VacuumEntityFeature
from homeassistant.const import (
    ATTR_CODE,
    ATTR_DEVICE_CLASS,
    ATTR_SUPPORTED_FEATURES,
    ATTR_UNIT_OF_MEASUREMENT,
    CONF_NAME,
    CONF_TYPE,
    LIGHT_LUX,
    PERCENTAGE,
    STATE_UNKNOWN,
    UnitOfTemperature,
)
from homeassistant.core import State


def get_identified_type(entity_id, attrs, config=None):
    """Helper to return the accessory type name selected by get_accessory."""

    def passthrough(type: type):
        return lambda *args, **kwargs: type

    # Patch TYPES so that get_accessory returns a type instead of an instance.
    with patch.dict(
        TYPES, {type_name: passthrough(v) for type_name, v in TYPES.items()}
    ):
        entity_state = State(entity_id, "irrelevant", attrs)
        return get_accessory(None, None, entity_state, 2, config or {})


def test_not_supported(caplog: pytest.LogCaptureFixture) -> None:
    """Test if none is returned if entity isn't supported."""
    # not supported entity
    assert get_accessory(None, None, State("demo.demo", "on"), 2, {}) is None

    # invalid aid
    assert get_accessory(None, None, State("light.demo", "on"), None, None) is None
    assert caplog.records[0].levelname == "WARNING"
    assert "invalid aid" in caplog.records[0].msg


def test_not_supported_sensor(caplog: pytest.LogCaptureFixture) -> None:
    """Test if none is returned if entity isn't supported."""
    assert get_accessory(None, None, State("sensor.xyz", "on"), 2, {}) is None
    assert "Unsupported sensor type (device_class=None)" in caplog.text


def test_not_supported_media_player() -> None:
    """Test if mode isn't supported and if no supported modes."""
    # selected mode for entity not supported
    config = {CONF_FEATURE_LIST: {FEATURE_ON_OFF: None}}
    entity_state = State("media_player.demo", "on")
    assert get_accessory(None, None, entity_state, 2, config) is None

    # no supported modes for entity
    entity_state = State("media_player.demo", "on")
    assert get_accessory(None, None, entity_state, 2, {}) is None


@pytest.mark.parametrize(
    ("config", "name"), [({CONF_NAME: "Customize Name"}, "Customize Name")]
)
def test_customize_options(config, name) -> None:
    """Test with customized options."""
    mock_type = Mock()
    conf = config.copy()
    conf[ATTR_INTEGRATION] = "platform_name"
    with patch.dict(TYPES, {"Light": mock_type}):
        entity_state = State("light.demo", "on")
        get_accessory(None, None, entity_state, 2, conf)
    mock_type.assert_called_with(None, None, name, "light.demo", 2, conf)


@pytest.mark.parametrize(
    ("type_name", "entity_id", "state", "attrs", "config"),
    [
        ("Fan", "fan.test", "on", {}, {}),
        ("Light", "light.test", "on", {}, {}),
        ("Lock", "lock.test", "locked", {}, {ATTR_CODE: "1234"}),
        (
            "SecuritySystem",
            "alarm_control_panel.test",
            "armed_away",
            {},
            {ATTR_CODE: "1234"},
        ),
        ("Thermostat", "climate.test", "auto", {}, {}),
        (
            "Thermostat",
            "climate.test",
            "auto",
            {ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE},
            {},
        ),
        ("HumidifierDehumidifier", "humidifier.test", "auto", {}, {}),
        ("WaterHeater", "water_heater.test", "auto", {}, {}),
    ],
)
def test_types(type_name, entity_id, state, attrs, config) -> None:
    """Test if types are associated correctly."""
    mock_type = Mock()
    with patch.dict(TYPES, {type_name: mock_type}):
        entity_state = State(entity_id, state, attrs)
        get_accessory(None, None, entity_state, 2, config)
    assert mock_type.called

    if config:
        assert mock_type.call_args[0][-1] == config


@pytest.mark.parametrize(
    ("type_name", "entity_id", "state", "attrs"),
    [
        (
            "GarageDoorOpener",
            "cover.garage_door",
            "open",
            {
                ATTR_DEVICE_CLASS: "garage",
                ATTR_SUPPORTED_FEATURES: CoverEntityFeature.OPEN
                | CoverEntityFeature.CLOSE,
            },
        ),
        (
            "Window",
            "cover.set_position",
            "open",
            {
                ATTR_DEVICE_CLASS: "window",
                ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION,
            },
        ),
        (
            "WindowCovering",
            "cover.set_position",
            "open",
            {ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION},
        ),
        (
            "WindowCovering",
            "cover.tilt",
            "open",
            {ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_TILT_POSITION},
        ),
        (
            "WindowCoveringBasic",
            "cover.open_window",
            "open",
            {
                ATTR_SUPPORTED_FEATURES: (
                    CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
                )
            },
        ),
        (
            "WindowCoveringBasic",
            "cover.open_window",
            "open",
            {
                ATTR_SUPPORTED_FEATURES: (
                    CoverEntityFeature.OPEN
                    | CoverEntityFeature.CLOSE
                    | CoverEntityFeature.SET_TILT_POSITION
                )
            },
        ),
        (
            "Door",
            "cover.door",
            "open",
            {
                ATTR_DEVICE_CLASS: "door",
                ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION,
            },
        ),
    ],
)
def test_type_covers(type_name, entity_id, state, attrs) -> None:
    """Test if cover types are associated correctly."""
    mock_type = Mock()
    with patch.dict(TYPES, {type_name: mock_type}):
        entity_state = State(entity_id, state, attrs)
        get_accessory(None, None, entity_state, 2, {})
    assert mock_type.called


@pytest.mark.parametrize(
    ("type_name", "entity_id", "state", "attrs", "config"),
    [
        (
            "MediaPlayer",
            "media_player.test",
            "on",
            {
                ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON
                | MediaPlayerEntityFeature.TURN_OFF
            },
            {CONF_FEATURE_LIST: {FEATURE_ON_OFF: None}},
        ),
        (
            "TelevisionMediaPlayer",
            "media_player.tv",
            "on",
            {ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV},
            {},
        ),
        (
            "ReceiverMediaPlayer",
            "media_player.receiver",
            "on",
            {ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.RECEIVER},
            {},
        ),
    ],
)
def test_type_media_player(type_name, entity_id, state, attrs, config) -> None:
    """Test if media_player types are associated correctly."""
    mock_type = Mock()
    with patch.dict(TYPES, {type_name: mock_type}):
        entity_state = State(entity_id, state, attrs)
        get_accessory(None, None, entity_state, 2, config)
    assert mock_type.called

    if config:
        assert mock_type.call_args[0][-1] == config


@pytest.mark.parametrize(
    ("type_name", "entity_id", "state", "attrs"),
    [
        ("BinarySensor", "binary_sensor.opening", "on", {ATTR_DEVICE_CLASS: "opening"}),
        ("BinarySensor", "device_tracker.someone", "not_home", {}),
        ("BinarySensor", "person.someone", "home", {}),
        ("PM10Sensor", "sensor.air_quality_pm10", "30", {}),
        (
            "PM10Sensor",
            "sensor.air_quality",
            "30",
            {ATTR_DEVICE_CLASS: "pm10"},
        ),
        ("PM25Sensor", "sensor.air_quality_pm25", "40", {}),
        (
            "PM25Sensor",
            "sensor.air_quality",
            "40",
            {ATTR_DEVICE_CLASS: "pm25"},
        ),
        (
            "NitrogenDioxideSensor",
            "sensor.air_quality_nitrogen_dioxide",
            "50",
            {ATTR_DEVICE_CLASS: SensorDeviceClass.NITROGEN_DIOXIDE},
        ),
        (
            "VolatileOrganicCompoundsSensor",
            "sensor.air_quality_volatile_organic_compounds",
            "55",
            {ATTR_DEVICE_CLASS: SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS},
        ),
        (
            "CarbonMonoxideSensor",
            "sensor.co",
            "2",
            {ATTR_DEVICE_CLASS: SensorDeviceClass.CO},
        ),
        ("CarbonDioxideSensor", "sensor.airmeter_co2", "500", {}),
        (
            "CarbonDioxideSensor",
            "sensor.co2",
            "500",
            {ATTR_DEVICE_CLASS: SensorDeviceClass.CO2},
        ),
        (
            "HumiditySensor",
            "sensor.humidity",
            "20",
            {ATTR_DEVICE_CLASS: "humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE},
        ),
        ("LightSensor", "sensor.light", "900", {ATTR_DEVICE_CLASS: "illuminance"}),
        ("LightSensor", "sensor.light", "900", {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX}),
        (
            "TemperatureSensor",
            "sensor.temperature",
            "23",
            {ATTR_DEVICE_CLASS: "temperature"},
        ),
        (
            "TemperatureSensor",
            "sensor.temperature",
            "23",
            {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS},
        ),
        (
            "TemperatureSensor",
            "sensor.temperature",
            "74",
            {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT},
        ),
    ],
)
def test_type_sensors(type_name, entity_id, state, attrs) -> None:
    """Test if sensor types are associated correctly."""
    mock_type = Mock()
    with patch.dict(TYPES, {type_name: mock_type}):
        entity_state = State(entity_id, state, attrs)
        get_accessory(None, None, entity_state, 2, {})
    assert mock_type.called


@pytest.mark.parametrize(
    ("type_name", "entity_id", "state", "attrs", "config"),
    [
        ("Outlet", "switch.test", "on", {}, {CONF_TYPE: TYPE_OUTLET}),
        (
            "Outlet",
            "switch.test",
            "on",
            {ATTR_DEVICE_CLASS: SwitchDeviceClass.OUTLET},
            {},
        ),
        ("Switch", "automation.test", "on", {}, {}),
        ("Switch", "button.test", STATE_UNKNOWN, {}, {}),
        ("Switch", "input_boolean.test", "on", {}, {}),
        ("Switch", "input_button.test", STATE_UNKNOWN, {}, {}),
        ("Switch", "remote.test", "on", {}, {}),
        ("Switch", "scene.test", "on", {}, {}),
        ("Switch", "script.test", "on", {}, {}),
        ("SelectSwitch", "input_select.test", "option1", {}, {}),
        ("SelectSwitch", "select.test", "option1", {}, {}),
        ("Switch", "switch.test", "on", {}, {}),
        ("Switch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SWITCH}),
        ("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_FAUCET}),
        ("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_VALVE}),
        ("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SHOWER}),
        ("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SPRINKLER}),
    ],
)
def test_type_switches(type_name, entity_id, state, attrs, config) -> None:
    """Test if switch types are associated correctly."""
    mock_type = Mock()
    with patch.dict(TYPES, {type_name: mock_type}):
        entity_state = State(entity_id, state, attrs)
        get_accessory(None, None, entity_state, 2, config)
    assert mock_type.called


@pytest.mark.parametrize(
    ("type_name", "entity_id", "state", "attrs", "config"),
    [
        ("Fan", "fan.test", "on", {}, {}),
        ("Fan", "fan.test", "on", {}, {CONF_TYPE: TYPE_FAN}),
        ("AirPurifier", "fan.test", "on", {}, {CONF_TYPE: TYPE_AIR_PURIFIER}),
    ],
)
def test_type_fans(type_name, entity_id, state, attrs, config) -> None:
    """Test if switch types are associated correctly."""
    mock_type = Mock()
    with patch.dict(TYPES, {type_name: mock_type}):
        entity_state = State(entity_id, state, attrs)
        get_accessory(None, None, entity_state, 2, config)
    assert mock_type.called


@pytest.mark.parametrize(
    ("type_name", "entity_id", "state", "attrs"),
    [
        ("Valve", "valve.test", "on", {}),
    ],
)
def test_type_valve(type_name, entity_id, state, attrs) -> None:
    """Test if valve types are associated correctly."""
    mock_type = Mock()
    with patch.dict(TYPES, {type_name: mock_type}):
        entity_state = State(entity_id, state, attrs)
        get_accessory(None, None, entity_state, 2, {})
    assert mock_type.called


@pytest.mark.parametrize(
    ("type_name", "entity_id", "state", "attrs"),
    [
        (
            "Vacuum",
            "vacuum.dock_vacuum",
            "docked",
            {
                ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.START
                | VacuumEntityFeature.RETURN_HOME
            },
        ),
        ("Vacuum", "vacuum.basic_vacuum", "off", {}),
    ],
)
def test_type_vacuum(type_name, entity_id, state, attrs) -> None:
    """Test if vacuum types are associated correctly."""
    mock_type = Mock()
    with patch.dict(TYPES, {type_name: mock_type}):
        entity_state = State(entity_id, state, attrs)
        get_accessory(None, None, entity_state, 2, {})
    assert mock_type.called


@pytest.mark.parametrize(
    ("type_name", "entity_id", "state", "attrs"),
    [("Camera", "camera.basic", "on", {})],
)
def test_type_camera(type_name, entity_id, state, attrs) -> None:
    """Test if camera types are associated correctly."""
    mock_type = Mock()
    with patch.dict(TYPES, {type_name: mock_type}):
        entity_state = State(entity_id, state, attrs)
        get_accessory(None, None, entity_state, 2, {})
    assert mock_type.called


@pytest.mark.parametrize(
    ("expected_type", "entity_id", "attrs"),
    [
        (
            PM10Sensor,
            "sensor.air_quality_pm25",
            {ATTR_DEVICE_CLASS: SensorDeviceClass.PM10},
        ),
        (
            PM25Sensor,
            "sensor.air_quality_pm10",
            {ATTR_DEVICE_CLASS: SensorDeviceClass.PM25},
        ),
        (
            AirQualitySensor,
            "sensor.co2_sensor",
            {ATTR_DEVICE_CLASS: SensorDeviceClass.GAS},
        ),
        (
            CarbonDioxideSensor,
            "sensor.air_quality_gas",
            {ATTR_DEVICE_CLASS: SensorDeviceClass.CO2},
        ),
        (
            TemperatureSensor,
            "sensor.random_sensor",
            {ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE},
        ),
    ],
)
def test_explicit_device_class_takes_precedence(
    expected_type, entity_id, attrs
) -> None:
    """Test that explicit device_class takes precedence over entity_id hints."""
    identified_type = get_identified_type(entity_id, attrs=attrs)
    assert identified_type == expected_type


@pytest.mark.parametrize(
    ("expected_type", "entity_id", "attrs"),
    [
        (PM10Sensor, "sensor.air_quality_pm10", {}),
        (PM25Sensor, "sensor.air_quality_pm25", {}),
        (AirQualitySensor, "sensor.air_quality_gas", {}),
        (CarbonDioxideSensor, "sensor.airmeter_co2", {}),
    ],
)
def test_entity_id_fallback_when_no_device_class(
    expected_type, entity_id, attrs
) -> None:
    """Test that entity_id is used as fallback when device_class is not set."""
    identified_type = get_identified_type(entity_id, attrs=attrs)
    assert identified_type == expected_type
