"""Test helpers for Hue."""

import asyncio
from collections import deque
from collections.abc import Generator
import logging
from typing import Any
from unittest.mock import AsyncMock, Mock, patch

import aiohue.v1 as aiohue_v1
import aiohue.v2 as aiohue_v2
from aiohue.v2.controllers.events import EventType
import pytest

from homeassistant.components import hue
from homeassistant.components.hue.v1 import sensor_base as hue_sensor_base
from homeassistant.components.hue.v2.device import async_setup_devices
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util.json import JsonArrayType

from .const import FAKE_BRIDGE, FAKE_BRIDGE_DEVICE

from tests.common import MockConfigEntry, load_json_array_fixture


@pytest.fixture(autouse=True)
def no_request_delay() -> Generator[None]:
    """Make the request refresh delay 0 for instant tests."""
    with patch("homeassistant.components.hue.const.REQUEST_REFRESH_DELAY", 0):
        yield


def create_mock_bridge(hass: HomeAssistant, api_version: int = 1) -> Mock:
    """Create a mocked HueBridge instance."""
    bridge = Mock(
        hass=hass,
        authorized=True,
        config_entry=None,
        reset_jobs=[],
        api_version=api_version,
        spec=hue.HueBridge,
    )

    bridge.logger = logging.getLogger(__name__)

    if bridge.api_version == 2:
        bridge.api = create_mock_api_v2()
        bridge.mock_requests = bridge.api.mock_requests
    else:
        bridge.api = create_mock_api_v1()
        bridge.sensor_manager = hue_sensor_base.SensorManager(bridge)
        bridge.mock_requests = bridge.api.mock_requests
        bridge.mock_light_responses = bridge.api.mock_light_responses
        bridge.mock_group_responses = bridge.api.mock_group_responses
        bridge.mock_sensor_responses = bridge.api.mock_sensor_responses

    async def async_initialize_bridge():
        if bridge.config_entry:
            bridge.config_entry.runtime_data = bridge
        if bridge.api_version == 2:
            await async_setup_devices(bridge)
        return True

    bridge.async_initialize_bridge = async_initialize_bridge

    async def async_request_call(task, *args, **kwargs):
        await task(*args, **kwargs)

    bridge.async_request_call = async_request_call

    async def async_reset():
        if bridge.config_entry:
            delattr(bridge.config_entry, "runtime_data")
        return True

    bridge.async_reset = async_reset

    return bridge


@pytest.fixture
def mock_api_v1() -> Mock:
    """Mock the Hue V1 api."""
    return create_mock_api_v1()


@pytest.fixture
def mock_api_v2() -> Mock:
    """Mock the Hue V2 api."""
    return create_mock_api_v2()


def create_mock_api_v1() -> Mock:
    """Create a mock V1 API."""
    api = Mock(spec=aiohue_v1.HueBridgeV1)
    api.initialize = AsyncMock()
    api.mock_requests = []
    api.mock_light_responses = deque()
    api.mock_group_responses = deque()
    api.mock_sensor_responses = deque()
    api.mock_scene_responses = deque()

    async def mock_request(method, path, **kwargs):
        kwargs["method"] = method
        kwargs["path"] = path
        api.mock_requests.append(kwargs)

        if path == "lights":
            return api.mock_light_responses.popleft()
        if path == "groups":
            return api.mock_group_responses.popleft()
        if path == "sensors":
            return api.mock_sensor_responses.popleft()
        if path == "scenes":
            return api.mock_scene_responses.popleft()
        return None

    logger = logging.getLogger(__name__)

    api.config = Mock(
        bridge_id="ff:ff:ff:ff:ff:ff",
        mac_address="aa:bb:cc:dd:ee:ff",
        model_id="BSB002",
        apiversion="9.9.9",
        software_version="1935144040",
    )
    api.config.name = "Home"
    # aiohue exposes the bridge id as both `bridge_id` and `bridgeid`
    api.config.bridgeid = api.config.bridge_id

    api.lights = aiohue_v1.Lights(logger, {}, mock_request)
    api.groups = aiohue_v1.Groups(logger, {}, mock_request)
    api.sensors = aiohue_v1.Sensors(logger, {}, mock_request)
    api.scenes = aiohue_v1.Scenes(logger, {}, mock_request)
    return api


@pytest.fixture(scope="package")
def v2_resources_test_data() -> JsonArrayType:
    """Load V2 resources mock data."""
    return load_json_array_fixture("hue/v2_resources.json")


def create_mock_api_v2() -> Mock:
    """Create a mock V2 API."""
    api = Mock(spec=aiohue_v2.HueBridgeV2)
    api.initialize = AsyncMock()
    api.mock_requests = []

    api.logger = logging.getLogger(__name__)
    api.config = aiohue_v2.ConfigController(api)
    api.events = aiohue_v2.EventStream(api)
    api.devices = aiohue_v2.DevicesController(api)
    api.lights = aiohue_v2.LightsController(api)
    api.sensors = aiohue_v2.SensorsController(api)
    api.groups = aiohue_v2.GroupsController(api)
    api.scenes = aiohue_v2.ScenesController(api)

    async def mock_request(method, path, **kwargs):
        kwargs["method"] = method
        kwargs["path"] = path
        api.mock_requests.append(kwargs)
        return kwargs.get("json")

    api.request = mock_request

    async def load_test_data(data: list[dict[str, Any]]):
        """Load test data into controllers."""

        # append default bridge if none explicitly given in test data
        if not any(x for x in data if x["type"] == "bridge"):
            data.append(FAKE_BRIDGE)
            data.append(FAKE_BRIDGE_DEVICE)

        await asyncio.gather(
            api.config.initialize(data),
            api.devices.initialize(data),
            api.lights.initialize(data),
            api.scenes.initialize(data),
            api.sensors.initialize(data),
            api.groups.initialize(data),
        )

    def emit_event(event_type, data):
        """Emit an event from a (hue resource) dict."""
        api.events.emit(EventType(event_type), data)

    api.load_test_data = load_test_data
    api.emit_event = emit_event
    # mock context manager too
    api.__aenter__ = AsyncMock(return_value=api)
    api.__aexit__ = AsyncMock()
    return api


@pytest.fixture
def mock_bridge_v1(hass: HomeAssistant) -> Mock:
    """Mock a Hue bridge with V1 api."""
    return create_mock_bridge(hass, api_version=1)


@pytest.fixture
def mock_bridge_v2(hass: HomeAssistant) -> Mock:
    """Mock a Hue bridge with V2 api."""
    return create_mock_bridge(hass, api_version=2)


@pytest.fixture
def mock_config_entry_v1() -> MockConfigEntry:
    """Mock a config entry for a Hue V1 bridge."""
    return create_config_entry(api_version=1)


@pytest.fixture
def mock_config_entry_v2() -> MockConfigEntry:
    """Mock a config entry."""
    return create_config_entry(api_version=2)


def create_config_entry(
    api_version: int = 1, host: str = "mock-host"
) -> MockConfigEntry:
    """Mock a config entry for a Hue bridge."""
    return MockConfigEntry(
        domain=hue.DOMAIN,
        title=f"Mock bridge {api_version}",
        data={"host": host, "api_version": api_version, "api_key": ""},
    )


async def setup_component(hass: HomeAssistant) -> None:
    """Mock setup Hue component."""
    with patch.object(hue, "async_setup_entry", return_value=True):
        assert (
            await async_setup_component(
                hass,
                hue.DOMAIN,
                {},
            )
            is True
        )


async def setup_bridge(
    hass: HomeAssistant, mock_bridge: Mock, config_entry: MockConfigEntry
) -> None:
    """Load the Hue integration with the provided bridge."""
    mock_bridge.config_entry = config_entry
    with patch.object(
        hue.migration, "is_v2_bridge", return_value=mock_bridge.api_version == 2
    ):
        config_entry.add_to_hass(hass)
        with patch("homeassistant.components.hue.HueBridge", return_value=mock_bridge):
            await hass.config_entries.async_setup(config_entry.entry_id)

    assert config_entry.state is ConfigEntryState.LOADED


async def setup_platform(
    hass: HomeAssistant,
    mock_bridge: Mock,
    platforms: list[Platform] | tuple[Platform] | Platform,
    hostname: str | None = None,
) -> None:
    """Load the Hue integration with the provided bridge for given platform(s)."""
    if not isinstance(platforms, (list, tuple)):
        platforms = [platforms]
    if hostname is None:
        hostname = "mock-host"
    hass.config.components.add(hue.DOMAIN)
    config_entry = create_config_entry(
        api_version=mock_bridge.api_version, host=hostname
    )
    mock_bridge.config_entry = config_entry
    config_entry.runtime_data = {config_entry.entry_id: mock_bridge}

    # simulate a full setup by manually adding the bridge config entry
    await setup_bridge(hass, mock_bridge, config_entry)
    assert await async_setup_component(hass, hue.DOMAIN, {}) is True
    await hass.async_block_till_done()

    config_entry.mock_state(hass, ConfigEntryState.LOADED)
    await hass.config_entries.async_forward_entry_setups(config_entry, platforms)

    # and make sure it completes before going further
    await hass.async_block_till_done()
