"""Test the Shark IQ vacuum entity."""

from collections.abc import Iterable
from copy import deepcopy
from datetime import datetime, timedelta
import enum
from typing import Any
from unittest.mock import patch

import pytest
from sharkiq import AylaApi, SharkIqAuthError, SharkIqNotAuthedError, SharkIqVacuum
from voluptuous.error import MultipleInvalid

from homeassistant import exceptions
from homeassistant.components.homeassistant import (
    DOMAIN as HOMEASSISTANT_DOMAIN,
    SERVICE_UPDATE_ENTITY,
)
from homeassistant.components.sharkiq.const import ATTR_ROOMS, DOMAIN
from homeassistant.components.sharkiq.services import SERVICE_CLEAN_ROOM
from homeassistant.components.sharkiq.vacuum import (
    ATTR_ERROR_CODE,
    ATTR_ERROR_MSG,
    ATTR_LOW_LIGHT,
    ATTR_RECHARGE_RESUME,
    FAN_SPEEDS_MAP,
)
from homeassistant.components.vacuum import (
    ATTR_BATTERY_LEVEL,
    ATTR_FAN_SPEED,
    ATTR_FAN_SPEED_LIST,
    SERVICE_LOCATE,
    SERVICE_PAUSE,
    SERVICE_RETURN_TO_BASE,
    SERVICE_SET_FAN_SPEED,
    SERVICE_START,
    SERVICE_STOP,
    VacuumActivity,
    VacuumEntityFeature,
)
from homeassistant.const import (
    ATTR_ENTITY_ID,
    ATTR_SUPPORTED_FEATURES,
    STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component

from .const import (
    CONFIG,
    ENTRY_ID,
    SHARK_DEVICE_DICT,
    SHARK_METADATA_DICT,
    SHARK_PROPERTIES_DICT,
    TEST_USERNAME,
)

from tests.common import MockConfigEntry

VAC_ENTITY_ID = f"vacuum.{SHARK_DEVICE_DICT['product_name'].lower()}"
ROOM_LIST = ["Kitchen", "Living Room"]
EXPECTED_FEATURES = (
    VacuumEntityFeature.BATTERY
    | VacuumEntityFeature.FAN_SPEED
    | VacuumEntityFeature.PAUSE
    | VacuumEntityFeature.RETURN_HOME
    | VacuumEntityFeature.START
    | VacuumEntityFeature.STATE
    | VacuumEntityFeature.STOP
    | VacuumEntityFeature.LOCATE
)


class MockAyla(AylaApi):
    """Mocked AylaApi that doesn't do anything."""

    desired_expiry = False

    async def async_sign_in(self):
        """Instead of signing in, just return."""

    async def async_set_cookie(self):
        """Instead of getting cookies, just return."""

    async def async_refresh_auth(self):
        """Instead of refreshing auth, just return."""

    async def async_sign_out(self):
        """Instead of signing out, just return."""

    async def async_list_devices(self) -> list[dict]:
        """Return the device list."""
        return [SHARK_DEVICE_DICT]

    async def async_get_devices(self, update: bool = True) -> list[SharkIqVacuum]:
        """Get the list of devices."""
        shark = MockShark(self, SHARK_DEVICE_DICT)
        shark.properties_full = deepcopy(SHARK_PROPERTIES_DICT)
        shark._update_metadata(SHARK_METADATA_DICT)
        return [shark]

    async def async_request(self, http_method: str, url: str, **kwargs):
        """Don't make an HTTP request."""

    @property
    def token_expiring_soon(self) -> bool:
        """Toggling Property for Token Expiration Flag."""
        # Alternate expiry flag for each test
        self.desired_expiry = not self.desired_expiry
        return self.desired_expiry

    @property
    def auth_expiration(self) -> datetime:
        """Sample expiration timestamp that is always 1200 seconds behind now()."""
        return datetime.now() - timedelta(seconds=1200)


class MockShark(SharkIqVacuum):
    """Mocked SharkIqVacuum that won't hit the API."""

    async def async_update(self, property_list: Iterable[str] | None = None):
        """Don't do anything."""

    def set_property_value(self, property_name, value):
        """Set a property locally without hitting the API."""
        if isinstance(property_name, enum.Enum):
            property_name = property_name.value
        if isinstance(value, enum.Enum):
            value = value.value
        self.properties_full[property_name]["value"] = value

    async def async_set_property_value(self, property_name, value):
        """Set a property locally without hitting the API."""
        self.set_property_value(property_name, value)

    def get_room_list(self):
        """Return the list of available rooms without hitting the API."""
        return ROOM_LIST


@pytest.fixture(autouse=True)
@patch("sharkiq.ayla_api.AylaApi", MockAyla)
async def setup_integration(hass: HomeAssistant) -> None:
    """Build the mock integration."""
    entry = MockConfigEntry(
        domain=DOMAIN, unique_id=TEST_USERNAME, data=CONFIG, entry_id=ENTRY_ID
    )
    entry.add_to_hass(hass)
    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()


async def test_simple_properties(
    hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
    """Test that simple properties work as intended."""
    state = hass.states.get(VAC_ENTITY_ID)
    entity = entity_registry.async_get(VAC_ENTITY_ID)

    assert entity
    assert state
    assert state.state == VacuumActivity.CLEANING
    assert entity.unique_id == "AC000Wxxxxxxxxx"


@pytest.mark.parametrize(
    ("attribute", "target_value"),
    [
        (ATTR_SUPPORTED_FEATURES, EXPECTED_FEATURES),
        (ATTR_BATTERY_LEVEL, 50),
        (ATTR_FAN_SPEED, "Eco"),
        (ATTR_FAN_SPEED_LIST, list(FAN_SPEEDS_MAP)),
        (ATTR_ERROR_CODE, 7),
        (ATTR_ERROR_MSG, "Cliff sensor is blocked"),
        (ATTR_LOW_LIGHT, False),
        (ATTR_RECHARGE_RESUME, True),
        (ATTR_ROOMS, ROOM_LIST),
    ],
)
async def test_initial_attributes(
    hass: HomeAssistant, attribute: str, target_value: Any
) -> None:
    """Test initial config attributes."""
    state = hass.states.get(VAC_ENTITY_ID)
    assert state.attributes.get(attribute) == target_value


@pytest.mark.parametrize(
    ("service", "target_state"),
    [
        (SERVICE_STOP, VacuumActivity.IDLE),
        (SERVICE_PAUSE, VacuumActivity.PAUSED),
        (SERVICE_RETURN_TO_BASE, VacuumActivity.RETURNING),
        (SERVICE_START, VacuumActivity.CLEANING),
    ],
)
async def test_cleaning_states(
    hass: HomeAssistant, service: str, target_state: str
) -> None:
    """Test cleaning states."""
    service_data = {ATTR_ENTITY_ID: VAC_ENTITY_ID}
    await hass.services.async_call("vacuum", service, service_data, blocking=True)
    state = hass.states.get(VAC_ENTITY_ID)
    assert state.state == target_state


@pytest.mark.parametrize("fan_speed", list(FAN_SPEEDS_MAP))
async def test_fan_speed(hass: HomeAssistant, fan_speed: str) -> None:
    """Test setting fan speeds."""
    service_data = {ATTR_ENTITY_ID: VAC_ENTITY_ID, ATTR_FAN_SPEED: fan_speed}
    await hass.services.async_call(
        "vacuum", SERVICE_SET_FAN_SPEED, service_data, blocking=True
    )
    state = hass.states.get(VAC_ENTITY_ID)
    assert state.attributes.get(ATTR_FAN_SPEED) == fan_speed


@pytest.mark.parametrize(
    ("device_property", "target_value"),
    [
        ("manufacturer", "Shark"),
        ("model", "RV1001AE"),
        ("name", "Sharknado"),
        ("sw_version", "Dummy Firmware 1.0"),
    ],
)
async def test_device_properties(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    device_property: str,
    target_value: str,
) -> None:
    """Test device properties."""
    device = device_registry.async_get_device(identifiers={(DOMAIN, "AC000Wxxxxxxxxx")})
    assert getattr(device, device_property) == target_value


@pytest.mark.parametrize(
    ("room_list", "exception"),
    [
        (["KITCHEN", "MUD_ROOM", "DOG HOUSE"], exceptions.ServiceValidationError),
        (["Office"], exceptions.ServiceValidationError),
        ([], MultipleInvalid),
    ],
)
async def test_clean_room_error(
    hass: HomeAssistant, room_list: list, exception: Exception
) -> None:
    """Test clean_room errors."""
    data = {ATTR_ENTITY_ID: VAC_ENTITY_ID, ATTR_ROOMS: room_list}

    with pytest.raises(exception):
        await hass.services.async_call(DOMAIN, SERVICE_CLEAN_ROOM, data, blocking=True)


async def test_locate(hass: HomeAssistant) -> None:
    """Test that the locate command works."""
    with patch.object(SharkIqVacuum, "async_find_device") as mock_locate:
        data = {ATTR_ENTITY_ID: VAC_ENTITY_ID}
        await hass.services.async_call("vacuum", SERVICE_LOCATE, data, blocking=True)
        mock_locate.assert_called_once()


@pytest.mark.parametrize(
    ("room_list"),
    [(ROOM_LIST), (["Kitchen"])],
)
async def test_clean_room(hass: HomeAssistant, room_list: list) -> None:
    """Test that the clean_room command works."""
    with patch.object(SharkIqVacuum, "async_clean_rooms") as mock_clean_room:
        data = {ATTR_ENTITY_ID: VAC_ENTITY_ID, ATTR_ROOMS: room_list}
        await hass.services.async_call(DOMAIN, SERVICE_CLEAN_ROOM, data, blocking=True)
        mock_clean_room.assert_called_once_with(room_list)


@pytest.mark.parametrize(
    ("side_effect", "success"),
    [
        (None, True),
        (SharkIqAuthError, False),
        (SharkIqNotAuthedError, False),
        (RuntimeError, False),
    ],
)
@patch("sharkiq.ayla_api.AylaApi", MockAyla)
async def test_coordinator_updates(
    hass: HomeAssistant, side_effect: Exception | None, success: bool
) -> None:
    """Test the update coordinator update functions."""
    entry = hass.config_entries.async_get_entry(ENTRY_ID)
    assert entry is not None
    coordinator = entry.runtime_data

    await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})

    with patch.object(
        MockShark, "async_update", side_effect=side_effect
    ) as mock_update:
        data = {ATTR_ENTITY_ID: [VAC_ENTITY_ID]}
        await hass.services.async_call(
            HOMEASSISTANT_DOMAIN, SERVICE_UPDATE_ENTITY, data, blocking=True
        )
        assert coordinator.last_update_success == success
        mock_update.assert_called_once()

    state = hass.states.get(VAC_ENTITY_ID)
    assert (state.state == STATE_UNAVAILABLE) != success
