"""Test HomeKit util module."""
from unittest.mock import MagicMock, Mock, patch

import pytest
import voluptuous as vol

from homeassistant.components.homekit.const import (
    BRIDGE_NAME,
    CONF_FEATURE,
    CONF_FEATURE_LIST,
    CONF_LINKED_BATTERY_SENSOR,
    CONF_LOW_BATTERY_THRESHOLD,
    DEFAULT_CONFIG_FLOW_PORT,
    DOMAIN,
    FEATURE_ON_OFF,
    FEATURE_PLAY_PAUSE,
    HOMEKIT_PAIRING_QR,
    HOMEKIT_PAIRING_QR_SECRET,
    TYPE_FAUCET,
    TYPE_OUTLET,
    TYPE_SHOWER,
    TYPE_SPRINKLER,
    TYPE_SWITCH,
    TYPE_VALVE,
)
from homeassistant.components.homekit.util import (
    accessory_friendly_name,
    async_find_next_available_port,
    async_port_is_available,
    cleanup_name_for_homekit,
    convert_to_float,
    density_to_air_quality,
    dismiss_setup_message,
    format_sw_version,
    show_setup_message,
    state_needs_accessory_mode,
    temperature_to_homekit,
    temperature_to_states,
    validate_entity_config as vec,
    validate_media_player_features,
)
from homeassistant.components.persistent_notification import (
    ATTR_MESSAGE,
    ATTR_NOTIFICATION_ID,
    DOMAIN as PERSISTENT_NOTIFICATION_DOMAIN,
)
from homeassistant.const import (
    ATTR_CODE,
    ATTR_SUPPORTED_FEATURES,
    CONF_NAME,
    CONF_PORT,
    CONF_TYPE,
    STATE_UNKNOWN,
    TEMP_CELSIUS,
    TEMP_FAHRENHEIT,
)
from homeassistant.core import State

from .util import async_init_integration

from tests.common import MockConfigEntry, async_mock_service


def _mock_socket(failure_attempts: int = 0) -> MagicMock:
    """Mock a socket that fails to bind failure_attempts amount of times."""
    mock_socket = MagicMock()
    attempts = 0

    def _simulate_bind(*_):
        nonlocal attempts
        attempts += 1
        if attempts <= failure_attempts:
            raise OSError
        return

    mock_socket.bind = Mock(side_effect=_simulate_bind)
    return mock_socket


def test_validate_entity_config():
    """Test validate entities."""
    configs = [
        None,
        [],
        "string",
        12345,
        {"invalid_entity_id": {}},
        {"demo.test": 1},
        {"binary_sensor.demo": {CONF_LINKED_BATTERY_SENSOR: None}},
        {"binary_sensor.demo": {CONF_LINKED_BATTERY_SENSOR: "switch.demo"}},
        {"binary_sensor.demo": {CONF_LOW_BATTERY_THRESHOLD: "switch.demo"}},
        {"binary_sensor.demo": {CONF_LOW_BATTERY_THRESHOLD: -10}},
        {"demo.test": "test"},
        {"demo.test": [1, 2]},
        {"demo.test": None},
        {"demo.test": {CONF_NAME: None}},
        {"media_player.test": {CONF_FEATURE_LIST: [{CONF_FEATURE: "invalid_feature"}]}},
        {
            "media_player.test": {
                CONF_FEATURE_LIST: [
                    {CONF_FEATURE: FEATURE_ON_OFF},
                    {CONF_FEATURE: FEATURE_ON_OFF},
                ]
            }
        },
        {"switch.test": {CONF_TYPE: "invalid_type"}},
    ]

    for conf in configs:
        with pytest.raises(vol.Invalid):
            vec(conf)

    assert vec({}) == {}
    assert vec({"demo.test": {CONF_NAME: "Name"}}) == {
        "demo.test": {CONF_NAME: "Name", CONF_LOW_BATTERY_THRESHOLD: 20}
    }

    assert vec(
        {"binary_sensor.demo": {CONF_LINKED_BATTERY_SENSOR: "sensor.demo_battery"}}
    ) == {
        "binary_sensor.demo": {
            CONF_LINKED_BATTERY_SENSOR: "sensor.demo_battery",
            CONF_LOW_BATTERY_THRESHOLD: 20,
        }
    }
    assert vec({"binary_sensor.demo": {CONF_LOW_BATTERY_THRESHOLD: 50}}) == {
        "binary_sensor.demo": {CONF_LOW_BATTERY_THRESHOLD: 50}
    }

    assert vec({"alarm_control_panel.demo": {}}) == {
        "alarm_control_panel.demo": {ATTR_CODE: None, CONF_LOW_BATTERY_THRESHOLD: 20}
    }
    assert vec({"alarm_control_panel.demo": {ATTR_CODE: "1234"}}) == {
        "alarm_control_panel.demo": {ATTR_CODE: "1234", CONF_LOW_BATTERY_THRESHOLD: 20}
    }

    assert vec({"lock.demo": {}}) == {
        "lock.demo": {ATTR_CODE: None, CONF_LOW_BATTERY_THRESHOLD: 20}
    }
    assert vec({"lock.demo": {ATTR_CODE: "1234"}}) == {
        "lock.demo": {ATTR_CODE: "1234", CONF_LOW_BATTERY_THRESHOLD: 20}
    }

    assert vec({"media_player.demo": {}}) == {
        "media_player.demo": {CONF_FEATURE_LIST: {}, CONF_LOW_BATTERY_THRESHOLD: 20}
    }
    config = {
        CONF_FEATURE_LIST: [
            {CONF_FEATURE: FEATURE_ON_OFF},
            {CONF_FEATURE: FEATURE_PLAY_PAUSE},
        ]
    }
    assert vec({"media_player.demo": config}) == {
        "media_player.demo": {
            CONF_FEATURE_LIST: {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}},
            CONF_LOW_BATTERY_THRESHOLD: 20,
        }
    }

    assert vec({"switch.demo": {CONF_TYPE: TYPE_FAUCET}}) == {
        "switch.demo": {CONF_TYPE: TYPE_FAUCET, CONF_LOW_BATTERY_THRESHOLD: 20}
    }
    assert vec({"switch.demo": {CONF_TYPE: TYPE_OUTLET}}) == {
        "switch.demo": {CONF_TYPE: TYPE_OUTLET, CONF_LOW_BATTERY_THRESHOLD: 20}
    }
    assert vec({"switch.demo": {CONF_TYPE: TYPE_SHOWER}}) == {
        "switch.demo": {CONF_TYPE: TYPE_SHOWER, CONF_LOW_BATTERY_THRESHOLD: 20}
    }
    assert vec({"switch.demo": {CONF_TYPE: TYPE_SPRINKLER}}) == {
        "switch.demo": {CONF_TYPE: TYPE_SPRINKLER, CONF_LOW_BATTERY_THRESHOLD: 20}
    }
    assert vec({"switch.demo": {CONF_TYPE: TYPE_SWITCH}}) == {
        "switch.demo": {CONF_TYPE: TYPE_SWITCH, CONF_LOW_BATTERY_THRESHOLD: 20}
    }
    assert vec({"switch.demo": {CONF_TYPE: TYPE_VALVE}}) == {
        "switch.demo": {CONF_TYPE: TYPE_VALVE, CONF_LOW_BATTERY_THRESHOLD: 20}
    }


def test_validate_media_player_features():
    """Test validate modes for media players."""
    config = {}
    attrs = {ATTR_SUPPORTED_FEATURES: 20873}
    entity_state = State("media_player.demo", "on", attrs)
    assert validate_media_player_features(entity_state, config) is True

    config = {FEATURE_ON_OFF: None}
    assert validate_media_player_features(entity_state, config) is True

    entity_state = State("media_player.demo", "on")
    assert validate_media_player_features(entity_state, config) is False


def test_convert_to_float():
    """Test convert_to_float method."""
    assert convert_to_float(12) == 12
    assert convert_to_float(12.4) == 12.4
    assert convert_to_float(STATE_UNKNOWN) is None
    assert convert_to_float(None) is None


def test_cleanup_name_for_homekit():
    """Ensure name sanitize works as expected."""

    assert cleanup_name_for_homekit("abc") == "abc"
    assert cleanup_name_for_homekit("a b c") == "a b c"
    assert cleanup_name_for_homekit("ab_c") == "ab c"
    assert (
        cleanup_name_for_homekit('ab!@#$%^&*()-=":.,><?//\\ frog')
        == "ab--#---&----- -.,------ frog"
    )
    assert cleanup_name_for_homekit("の日本_語文字セット") == "の日本 語文字セット"


def test_temperature_to_homekit():
    """Test temperature conversion from HA to HomeKit."""
    assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5
    assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.4


def test_temperature_to_states():
    """Test temperature conversion from HomeKit to HA."""
    assert temperature_to_states(20, TEMP_CELSIUS) == 20.0
    assert temperature_to_states(20.2, TEMP_FAHRENHEIT) == 68.5


def test_density_to_air_quality():
    """Test map PM2.5 density to HomeKit AirQuality level."""
    assert density_to_air_quality(0) == 1
    assert density_to_air_quality(35) == 1
    assert density_to_air_quality(35.1) == 2
    assert density_to_air_quality(75) == 2
    assert density_to_air_quality(115) == 3
    assert density_to_air_quality(150) == 4
    assert density_to_air_quality(300) == 5


async def test_show_setup_msg(hass, hk_driver, mock_get_source_ip):
    """Test show setup message as persistence notification."""
    pincode = b"123-45-678"

    entry = await async_init_integration(hass)
    assert entry

    call_create_notification = async_mock_service(
        hass, PERSISTENT_NOTIFICATION_DOMAIN, "create"
    )

    await hass.async_add_executor_job(
        show_setup_message, hass, entry.entry_id, "bridge_name", pincode, "X-HM://0"
    )
    await hass.async_block_till_done()
    assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR_SECRET]
    assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR]

    assert call_create_notification
    assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == entry.entry_id
    assert pincode.decode() in call_create_notification[0].data[ATTR_MESSAGE]


async def test_dismiss_setup_msg(hass):
    """Test dismiss setup message."""
    call_dismiss_notification = async_mock_service(
        hass, PERSISTENT_NOTIFICATION_DOMAIN, "dismiss"
    )

    await hass.async_add_executor_job(dismiss_setup_message, hass, "entry_id")
    await hass.async_block_till_done()

    assert call_dismiss_notification
    assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == "entry_id"


async def test_port_is_available(hass):
    """Test we can get an available port and it is actually available."""
    with patch(
        "homeassistant.components.homekit.util.socket.socket",
        return_value=_mock_socket(0),
    ):
        next_port = async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT)
    assert next_port
    with patch(
        "homeassistant.components.homekit.util.socket.socket",
        return_value=_mock_socket(0),
    ):
        assert async_port_is_available(next_port)

    with patch(
        "homeassistant.components.homekit.util.socket.socket",
        return_value=_mock_socket(5),
    ):
        next_port = async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT)
    assert next_port == DEFAULT_CONFIG_FLOW_PORT + 5
    with patch(
        "homeassistant.components.homekit.util.socket.socket",
        return_value=_mock_socket(0),
    ):
        assert async_port_is_available(next_port)

    with patch(
        "homeassistant.components.homekit.util.socket.socket",
        return_value=_mock_socket(1),
    ):
        assert not async_port_is_available(next_port)


async def test_port_is_available_skips_existing_entries(hass):
    """Test we can get an available port and it is actually available."""
    entry = MockConfigEntry(
        domain=DOMAIN,
        data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_CONFIG_FLOW_PORT},
        options={},
    )
    entry.add_to_hass(hass)

    with patch(
        "homeassistant.components.homekit.util.socket.socket",
        return_value=_mock_socket(),
    ):
        next_port = async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT)

    assert next_port == DEFAULT_CONFIG_FLOW_PORT + 1

    with patch(
        "homeassistant.components.homekit.util.socket.socket",
        return_value=_mock_socket(),
    ):
        assert async_port_is_available(next_port)

    with patch(
        "homeassistant.components.homekit.util.socket.socket",
        return_value=_mock_socket(4),
    ):
        next_port = async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT)

    assert next_port == DEFAULT_CONFIG_FLOW_PORT + 5
    with patch(
        "homeassistant.components.homekit.util.socket.socket",
        return_value=_mock_socket(),
    ):
        assert async_port_is_available(next_port)

    with pytest.raises(OSError), patch(
        "homeassistant.components.homekit.util.socket.socket",
        return_value=_mock_socket(10),
    ):
        async_find_next_available_port(hass, 65530)


async def test_format_sw_version():
    """Test format_sw_version method."""
    assert format_sw_version("soho+3.6.8+soho-release-rt120+10") == "3.6.8"
    assert format_sw_version("undefined-undefined-1.6.8") == "1.6.8"
    assert format_sw_version("56.0-76060") == "56.0.76060"
    assert format_sw_version(3.6) == "3.6"
    assert format_sw_version("unknown") is None


async def test_accessory_friendly_name():
    """Test we provide a helpful friendly name."""

    accessory = Mock()
    accessory.display_name = "same"
    assert accessory_friendly_name("Same", accessory) == "Same"
    assert accessory_friendly_name("hass title", accessory) == "hass title (same)"
    accessory.display_name = "Hass title 123"
    assert accessory_friendly_name("hass title", accessory) == "Hass title 123"


async def test_lock_state_needs_accessory_mode(hass):
    """Test that locks are setup as accessories."""
    hass.states.async_set("lock.mine", "locked")
    assert state_needs_accessory_mode(hass.states.get("lock.mine")) is True
