"""Test the Insteon properties APIs."""

import asyncio
import json
from typing import Any
from unittest.mock import AsyncMock, patch

from pyinsteon.config import MOMENTARY_DELAY, RELAY_MODE, TOGGLE_BUTTON
from pyinsteon.config.extended_property import ExtendedProperty
from pyinsteon.constants import RelayMode, ToggleMode
import pytest

from homeassistant.components import insteon
from homeassistant.components.insteon.api import async_load_api
from homeassistant.components.insteon.api.device import INSTEON_DEVICE_NOT_FOUND
from homeassistant.components.insteon.api.properties import (
    DEVICE_ADDRESS,
    ID,
    PROPERTY_NAME,
    PROPERTY_VALUE,
    RADIO_BUTTON_GROUPS,
    RAMP_RATE_IN_SEC,
    SHOW_ADVANCED,
    TYPE,
)
from homeassistant.core import HomeAssistant

from .mock_devices import MockDevices

from tests.common import load_fixture
from tests.typing import MockHAClientWebSocket, WebSocketGenerator


@pytest.fixture(name="kpl_properties_data", scope="module")
def kpl_properties_data_fixture():
    """Load the controller state fixture data."""
    return json.loads(load_fixture("insteon/kpl_properties.json"))


@pytest.fixture(name="iolinc_properties_data", scope="module")
def iolinc_properties_data_fixture():
    """Load the controller state fixture data."""
    return json.loads(load_fixture("insteon/iolinc_properties.json"))


async def _setup(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    address: str,
    properties_data: dict[str, Any],
) -> tuple[MockHAClientWebSocket, MockDevices]:
    """Set up tests."""
    ws_client = await hass_ws_client(hass)
    devices = MockDevices()
    await devices.async_load()
    devices.fill_properties(address, properties_data)
    async_load_api(hass)
    return ws_client, devices


async def test_get_properties(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    kpl_properties_data,
    iolinc_properties_data,
) -> None:
    """Test getting an Insteon device's properties."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )
    devices.fill_properties("44.44.44", iolinc_properties_data)

    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {
                ID: 2,
                TYPE: "insteon/properties/get",
                DEVICE_ADDRESS: "33.33.33",
                SHOW_ADVANCED: False,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert len(msg["result"]["properties"]) == 18

        await ws_client.send_json(
            {
                ID: 3,
                TYPE: "insteon/properties/get",
                DEVICE_ADDRESS: "44.44.44",
                SHOW_ADVANCED: False,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert len(msg["result"]["properties"]) == 6

        await ws_client.send_json(
            {
                ID: 4,
                TYPE: "insteon/properties/get",
                DEVICE_ADDRESS: "33.33.33",
                SHOW_ADVANCED: True,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert len(msg["result"]["properties"]) == 69

        await ws_client.send_json(
            {
                ID: 5,
                TYPE: "insteon/properties/get",
                DEVICE_ADDRESS: "44.44.44",
                SHOW_ADVANCED: True,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert len(msg["result"]["properties"]) == 14


async def test_get_read_only_properties(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, iolinc_properties_data
) -> None:
    """Test getting an Insteon device's properties."""
    mock_read_only = ExtendedProperty(
        "44.44.44", "mock_read_only", bool, is_read_only=True
    )
    mock_read_only.set_value(False)

    ws_client, devices = await _setup(
        hass, hass_ws_client, "44.44.44", iolinc_properties_data
    )
    device = devices["44.44.44"]
    device.configuration["mock_read_only"] = mock_read_only
    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {
                ID: 2,
                TYPE: "insteon/properties/get",
                DEVICE_ADDRESS: "44.44.44",
                SHOW_ADVANCED: False,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert len(msg["result"]["properties"]) == 6
        await ws_client.send_json(
            {
                ID: 3,
                TYPE: "insteon/properties/get",
                DEVICE_ADDRESS: "44.44.44",
                SHOW_ADVANCED: True,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert len(msg["result"]["properties"]) == 15
    await asyncio.sleep(1)


async def test_get_unknown_properties(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, iolinc_properties_data
) -> None:
    """Test getting an Insteon device's properties."""

    class UnknownType:
        """Mock unknown data type."""

    mock_unknown = ExtendedProperty("44.44.44", "mock_unknown", UnknownType)

    ws_client, devices = await _setup(
        hass, hass_ws_client, "44.44.44", iolinc_properties_data
    )
    device = devices["44.44.44"]
    device.configuration["mock_unknown"] = mock_unknown
    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {
                ID: 2,
                TYPE: "insteon/properties/get",
                DEVICE_ADDRESS: "44.44.44",
                SHOW_ADVANCED: False,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert len(msg["result"]["properties"]) == 6
        await ws_client.send_json(
            {
                ID: 3,
                TYPE: "insteon/properties/get",
                DEVICE_ADDRESS: "44.44.44",
                SHOW_ADVANCED: True,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert len(msg["result"]["properties"]) == 14


async def test_change_bool_property(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Test changing a bool type properties."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )

    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {
                ID: 3,
                TYPE: "insteon/properties/change",
                DEVICE_ADDRESS: "33.33.33",
                PROPERTY_NAME: "led_off",
                PROPERTY_VALUE: True,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert devices["33.33.33"].operating_flags["led_off"].is_dirty


async def test_change_int_property(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Test changing a int type properties."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )

    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {
                ID: 4,
                TYPE: "insteon/properties/change",
                DEVICE_ADDRESS: "33.33.33",
                PROPERTY_NAME: "led_dimming",
                PROPERTY_VALUE: 100,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert devices["33.33.33"].properties["led_dimming"].new_value == 100
        assert devices["33.33.33"].properties["led_dimming"].is_dirty


async def test_change_ramp_rate_property(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Test changing an Insteon device's ramp rate properties."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )

    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {
                ID: 2,
                TYPE: "insteon/properties/change",
                DEVICE_ADDRESS: "33.33.33",
                PROPERTY_NAME: RAMP_RATE_IN_SEC,
                PROPERTY_VALUE: 4.5,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert devices["33.33.33"].properties["ramp_rate"].new_value == 0x1A
        assert devices["33.33.33"].properties["ramp_rate"].is_dirty


async def test_change_radio_button_group(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Test changing an Insteon device's properties."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )
    rb_groups = devices["33.33.33"].configuration[RADIO_BUTTON_GROUPS]

    # Make sure the baseline is correct
    assert rb_groups.value[0] == [4, 5]
    assert rb_groups.value[1] == [7, 8]

    # Add button 1 to the group
    new_groups_1 = [[1, 4, 5], [7, 8]]
    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {
                ID: 2,
                TYPE: "insteon/properties/change",
                DEVICE_ADDRESS: "33.33.33",
                PROPERTY_NAME: RADIO_BUTTON_GROUPS,
                PROPERTY_VALUE: new_groups_1,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert rb_groups.new_value[0] == [1, 4, 5]
        assert rb_groups.new_value[1] == [7, 8]

        new_groups_2 = [[1, 4], [7, 8]]
        await ws_client.send_json(
            {
                ID: 3,
                TYPE: "insteon/properties/change",
                DEVICE_ADDRESS: "33.33.33",
                PROPERTY_NAME: RADIO_BUTTON_GROUPS,
                PROPERTY_VALUE: new_groups_2,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert rb_groups.new_value[0] == [1, 4]
        assert rb_groups.new_value[1] == [7, 8]


async def test_change_toggle_property(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Update a button's toggle mode."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )
    device = devices["33.33.33"]
    prop_name = f"{TOGGLE_BUTTON}_c"
    toggle_prop = device.configuration[prop_name]
    assert toggle_prop.value == ToggleMode.TOGGLE
    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {
                ID: 2,
                TYPE: "insteon/properties/change",
                DEVICE_ADDRESS: "33.33.33",
                PROPERTY_NAME: prop_name,
                PROPERTY_VALUE: str(ToggleMode.ON_ONLY).lower(),
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert toggle_prop.new_value == ToggleMode.ON_ONLY


async def test_change_relay_mode(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, iolinc_properties_data
) -> None:
    """Update a device's relay mode."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "44.44.44", iolinc_properties_data
    )
    device = devices["44.44.44"]
    relay_prop = device.configuration[RELAY_MODE]
    assert relay_prop.value == RelayMode.MOMENTARY_A
    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {
                ID: 2,
                TYPE: "insteon/properties/change",
                DEVICE_ADDRESS: "44.44.44",
                PROPERTY_NAME: RELAY_MODE,
                PROPERTY_VALUE: str(RelayMode.LATCHING).lower(),
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert relay_prop.new_value == RelayMode.LATCHING


async def test_change_float_property(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, iolinc_properties_data
) -> None:
    """Update a float type property."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "44.44.44", iolinc_properties_data
    )
    device = devices["44.44.44"]
    delay_prop = device.configuration[MOMENTARY_DELAY]
    delay_prop.set_value(0)
    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {
                ID: 2,
                TYPE: "insteon/properties/change",
                DEVICE_ADDRESS: "44.44.44",
                PROPERTY_NAME: MOMENTARY_DELAY,
                PROPERTY_VALUE: 1.8,
            }
        )
        msg = await ws_client.receive_json()
        assert msg["success"]

        assert delay_prop.new_value == 1.8


async def test_write_properties(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Test getting an Insteon device's properties."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )

    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {ID: 2, TYPE: "insteon/properties/write", DEVICE_ADDRESS: "33.33.33"}
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert devices["33.33.33"].async_write_op_flags.call_count == 1
        assert devices["33.33.33"].async_write_ext_properties.call_count == 1


async def test_write_properties_failure(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Test getting an Insteon device's properties."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )

    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {ID: 2, TYPE: "insteon/properties/write", DEVICE_ADDRESS: "22.22.22"}
        )
        msg = await ws_client.receive_json()
        assert not msg["success"]
        assert msg["error"]["code"] == "write_failed"


async def test_load_properties(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Test getting an Insteon device's properties."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )

    device = devices["33.33.33"]
    device.async_read_config = AsyncMock(return_value=1)
    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"}
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert devices["33.33.33"].async_read_config.call_count == 1


async def test_load_properties_failure(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Test getting an Insteon device's properties."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )

    device = devices["33.33.33"]
    device.async_read_config = AsyncMock(return_value=0)
    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"}
        )
        msg = await ws_client.receive_json()
        assert not msg["success"]
        assert msg["error"]["code"] == "load_failed"


async def test_reset_properties(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Test getting an Insteon device's properties."""
    ws_client, devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )

    device = devices["33.33.33"]
    device.configuration["led_off"].new_value = True
    device.properties["on_mask"].new_value = 100
    assert device.operating_flags["led_off"].is_dirty
    assert device.properties["on_mask"].is_dirty
    with patch.object(insteon.api.properties, "devices", devices):
        await ws_client.send_json(
            {ID: 2, TYPE: "insteon/properties/reset", DEVICE_ADDRESS: "33.33.33"}
        )
        msg = await ws_client.receive_json()
        assert msg["success"]
        assert not device.operating_flags["led_off"].is_dirty
        assert not device.properties["on_mask"].is_dirty


async def test_bad_address(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, kpl_properties_data
) -> None:
    """Test for a bad Insteon address."""
    ws_client, _devices = await _setup(
        hass, hass_ws_client, "33.33.33", kpl_properties_data
    )

    ws_id = 0
    for call in ("get", "write", "load", "reset"):
        ws_id += 1
        params = {
            ID: ws_id,
            TYPE: f"insteon/properties/{call}",
            DEVICE_ADDRESS: "99.99.99",
        }
        if call == "get":
            params[SHOW_ADVANCED] = False
        await ws_client.send_json(params)
        msg = await ws_client.receive_json()
        assert not msg["success"]
        assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND

    ws_id += 1
    await ws_client.send_json(
        {
            ID: ws_id,
            TYPE: "insteon/properties/change",
            DEVICE_ADDRESS: "99.99.99",
            PROPERTY_NAME: "led_off",
            PROPERTY_VALUE: True,
        }
    )
    msg = await ws_client.receive_json()
    assert not msg["success"]
    assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND
