"""Tests for the UniFi Access integration setup."""

from collections.abc import Awaitable, Callable
import ssl
from unittest.mock import MagicMock, patch

import pytest
from unifi_access_api import (
    ApiAuthError,
    ApiConnectionError,
    ApiError,
    DoorPositionStatus,
)
from unifi_access_api.models.websocket import (
    LocationUpdateData,
    LocationUpdateState,
    LocationUpdateV2,
    ThumbnailInfo,
    V2LocationState,
    V2LocationUpdate,
    V2LocationUpdateData,
    WebsocketMessage,
)

from homeassistant.components.unifi_access.const import DOMAIN, SERVICE_SET_LOCK_RULE
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr

from .conftest import _make_door

from tests.common import MockConfigEntry

FRONT_DOOR_BINARY_SENSOR = "binary_sensor.front_door"
BACK_DOOR_BINARY_SENSOR = "binary_sensor.back_door"
FRONT_DOOR_IMAGE = "image.front_door_thumbnail"
BACK_DOOR_IMAGE = "image.back_door_thumbnail"


def _get_ws_handlers(
    mock_client: MagicMock,
) -> dict[str, Callable[[WebsocketMessage], Awaitable[None]]]:
    """Extract WebSocket handlers from mock client."""
    return mock_client.start_websocket.call_args[0][0]


async def test_setup_entry(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test successful setup of a config entry."""
    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.LOADED
    mock_client.authenticate.assert_awaited_once()
    mock_client.get_doors.assert_awaited_once()
    assert hass.services.has_service(DOMAIN, SERVICE_SET_LOCK_RULE)


@pytest.mark.parametrize(
    ("verify_ssl", "expected_ssl_context_type"),
    [
        (False, ssl.SSLContext),
        (True, type(None)),
    ],
)
async def test_setup_entry_ssl_context(
    hass: HomeAssistant,
    mock_client: MagicMock,
    verify_ssl: bool,
    expected_ssl_context_type: type,
) -> None:
    """Test no-verify SSL context is passed when verify_ssl is False."""
    entry = MockConfigEntry(
        domain=DOMAIN,
        title="UniFi Access",
        data={
            "host": "192.168.1.1",
            "api_token": "test-token",
            CONF_VERIFY_SSL: verify_ssl,
        },
        version=1,
        minor_version=1,
    )
    entry.add_to_hass(hass)

    with patch(
        "homeassistant.components.unifi_access.UnifiAccessApiClient",
        wraps=lambda **kwargs: mock_client,
    ) as patched_client:
        await hass.config_entries.async_setup(entry.entry_id)
        await hass.async_block_till_done()

    _, call_kwargs = patched_client.call_args
    assert isinstance(call_kwargs["ssl_context"], expected_ssl_context_type)


@pytest.mark.parametrize(
    ("exception", "expected_state"),
    [
        (ApiAuthError(), ConfigEntryState.SETUP_ERROR),
        (ApiConnectionError("Connection failed"), ConfigEntryState.SETUP_RETRY),
    ],
)
async def test_setup_entry_error(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_client: MagicMock,
    exception: Exception,
    expected_state: ConfigEntryState,
) -> None:
    """Test setup handles errors correctly."""
    mock_client.authenticate.side_effect = exception
    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is expected_state

    if expected_state is ConfigEntryState.SETUP_ERROR:
        assert any(
            flow["context"]["source"] == SOURCE_REAUTH
            for flow in hass.config_entries.flow.async_progress()
        )


@pytest.mark.parametrize(
    ("failing_method", "exception", "expected_state"),
    [
        ("get_doors", ApiAuthError(), ConfigEntryState.SETUP_ERROR),
        (
            "get_doors",
            ApiConnectionError("Connection failed"),
            ConfigEntryState.SETUP_RETRY,
        ),
        ("get_doors", ApiError("API error"), ConfigEntryState.SETUP_RETRY),
        ("get_emergency_status", ApiAuthError(), ConfigEntryState.SETUP_ERROR),
        (
            "get_emergency_status",
            ApiConnectionError("Connection failed"),
            ConfigEntryState.SETUP_RETRY,
        ),
        ("get_emergency_status", ApiError("API error"), ConfigEntryState.SETUP_RETRY),
    ],
)
async def test_coordinator_update_error(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_client: MagicMock,
    failing_method: str,
    exception: Exception,
    expected_state: ConfigEntryState,
) -> None:
    """Test coordinator handles update errors from get_doors or get_emergency_status."""
    getattr(mock_client, failing_method).side_effect = exception
    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is expected_state

    if expected_state is ConfigEntryState.SETUP_ERROR:
        assert any(
            flow["context"]["source"] == SOURCE_REAUTH
            for flow in hass.config_entries.flow.async_progress()
        )


async def test_unload_entry(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test unloading a config entry."""
    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.LOADED

    await hass.config_entries.async_unload(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.NOT_LOADED


async def test_ws_location_update_v2(
    hass: HomeAssistant,
    init_integration: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test location_update_v2 WebSocket message updates door state."""
    assert hass.states.get(FRONT_DOOR_BINARY_SENSOR).state == "off"

    handlers = _get_ws_handlers(mock_client)
    msg = LocationUpdateV2(
        event="access.data.device.location_update_v2",
        data=LocationUpdateData(
            id="door-001",
            location_type="DOOR",
            state=LocationUpdateState(
                dps=DoorPositionStatus.OPEN,
                lock="unlocked",
            ),
        ),
    )

    await handlers["access.data.device.location_update_v2"](msg)
    await hass.async_block_till_done()

    assert hass.states.get(FRONT_DOOR_BINARY_SENSOR).state == "on"


async def test_ws_v2_location_update(
    hass: HomeAssistant,
    init_integration: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test V2 location update WebSocket message updates door state."""
    assert hass.states.get(BACK_DOOR_BINARY_SENSOR).state == "on"

    handlers = _get_ws_handlers(mock_client)
    msg = V2LocationUpdate(
        event="access.data.v2.location.update",
        data=V2LocationUpdateData(
            id="door-002",
            location_type="DOOR",
            name="Back Door",
            up_id="up-1",
            device_ids=[],
            state=V2LocationState(
                lock="locked",
                dps=DoorPositionStatus.CLOSE,
                dps_connected=True,
                is_unavailable=False,
            ),
        ),
    )

    await handlers["access.data.v2.location.update"](msg)
    await hass.async_block_till_done()

    assert hass.states.get(BACK_DOOR_BINARY_SENSOR).state == "off"


async def test_ws_location_update_unknown_door_ignored(
    hass: HomeAssistant,
    init_integration: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test location update for unknown door is silently ignored."""
    state_before = hass.states.get(FRONT_DOOR_BINARY_SENSOR).state

    handlers = _get_ws_handlers(mock_client)
    msg = LocationUpdateV2(
        event="access.data.device.location_update_v2",
        data=LocationUpdateData(
            id="door-unknown",
            location_type="DOOR",
            state=LocationUpdateState(
                dps=DoorPositionStatus.OPEN,
                lock="unlocked",
            ),
        ),
    )

    await handlers["access.data.device.location_update_v2"](msg)
    await hass.async_block_till_done()

    assert hass.states.get(FRONT_DOOR_BINARY_SENSOR).state == state_before


async def test_ws_location_update_no_state_ignored(
    hass: HomeAssistant,
    init_integration: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test location update with no state is silently ignored."""
    state_before = hass.states.get(FRONT_DOOR_BINARY_SENSOR).state

    handlers = _get_ws_handlers(mock_client)
    msg = LocationUpdateV2(
        event="access.data.device.location_update_v2",
        data=LocationUpdateData(
            id="door-001",
            location_type="DOOR",
            state=None,
        ),
    )

    await handlers["access.data.device.location_update_v2"](msg)
    await hass.async_block_till_done()

    assert hass.states.get(FRONT_DOOR_BINARY_SENSOR).state == state_before


async def test_ws_location_update_no_op_state_ignored(
    hass: HomeAssistant,
    init_integration: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test location update with state but no relevant fields is ignored."""
    state_before = hass.states.get(FRONT_DOOR_BINARY_SENSOR).state

    handlers = _get_ws_handlers(mock_client)
    msg = LocationUpdateV2(
        event="access.data.device.location_update_v2",
        data=LocationUpdateData(
            id="door-001",
            location_type="DOOR",
            state=LocationUpdateState.model_construct(
                dps=None,
                lock="unknown",
            ),
        ),
    )

    await handlers["access.data.device.location_update_v2"](msg)
    await hass.async_block_till_done()

    assert hass.states.get(FRONT_DOOR_BINARY_SENSOR).state == state_before


async def test_ws_location_update_with_thumbnail(
    hass: HomeAssistant,
    init_integration: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test location_update_v2 with thumbnail updates image entity."""
    # Back door starts without thumbnail — entity does not exist yet
    assert hass.states.get(BACK_DOOR_IMAGE) is None

    handlers = _get_ws_handlers(mock_client)
    msg = LocationUpdateV2(
        event="access.data.device.location_update_v2",
        data=LocationUpdateData(
            id="door-002",
            location_type="DOOR",
            state=None,
            thumbnail=ThumbnailInfo(
                url="/thumb/door-002.jpg",
                door_thumbnail_last_update=1700000000,
            ),
        ),
    )

    await handlers["access.data.device.location_update_v2"](msg)
    await hass.async_block_till_done()

    assert hass.states.get(BACK_DOOR_IMAGE).state != "unknown"


async def test_coordinator_timeout_error(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test coordinator handles timeout from API."""
    mock_client.get_doors.side_effect = TimeoutError
    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY


async def test_ws_location_update_thumbnail_only_no_state(
    hass: HomeAssistant,
    init_integration: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test location update with thumbnail but no state keeps door unchanged."""
    state_before = hass.states.get(FRONT_DOOR_BINARY_SENSOR).state
    image_state_before = hass.states.get(FRONT_DOOR_IMAGE).state

    handlers = _get_ws_handlers(mock_client)
    msg = LocationUpdateV2(
        event="access.data.device.location_update_v2",
        data=LocationUpdateData(
            id="door-001",
            location_type="DOOR",
            state=None,
            thumbnail=ThumbnailInfo(
                url="/thumb/door-001-new.jpg",
                door_thumbnail_last_update=1700002000,
            ),
        ),
    )

    await handlers["access.data.device.location_update_v2"](msg)
    await hass.async_block_till_done()

    # Door state unchanged, thumbnail updated
    assert hass.states.get(FRONT_DOOR_BINARY_SENSOR).state == state_before
    assert hass.states.get(FRONT_DOOR_IMAGE).state != image_state_before


async def test_new_door_entities_created_on_refresh(
    hass: HomeAssistant,
    init_integration: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test that new door entities are added dynamically via coordinator listener."""
    # Verify new door entities do not exist yet
    assert not hass.states.get("binary_sensor.garage_door")
    assert not hass.states.get("button.garage_door_unlock")
    assert not hass.states.get("event.garage_door_doorbell")
    assert not hass.states.get("event.garage_door_access")
    assert not hass.states.get("image.garage_door_thumbnail")

    # Add a new door to the API response
    mock_client.get_doors.return_value = [
        *mock_client.get_doors.return_value,
        _make_door(
            "door-003",
            "Garage Door",
            door_thumbnail="/preview/garage_door.png",
            door_thumbnail_last_update=1700000000,
        ),
    ]

    # Trigger natural refresh via WebSocket reconnect
    on_disconnect = mock_client.start_websocket.call_args[1]["on_disconnect"]
    on_connect = mock_client.start_websocket.call_args[1]["on_connect"]
    on_disconnect()
    await hass.async_block_till_done()
    on_connect()
    await hass.async_block_till_done()

    # Entities for the new door should now exist
    assert hass.states.get("binary_sensor.garage_door")
    assert hass.states.get("button.garage_door_unlock")
    assert hass.states.get("event.garage_door_doorbell")
    assert hass.states.get("event.garage_door_access")
    assert hass.states.get("image.garage_door_thumbnail")


async def test_stale_device_removed_on_refresh(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    init_integration: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test that stale devices are automatically removed on data refresh."""
    # Verify both doors exist after initial setup
    assert device_registry.async_get_device(identifiers={(DOMAIN, "door-001")})
    assert device_registry.async_get_device(identifiers={(DOMAIN, "door-002")})

    # Simulate door-002 being removed from the hub
    mock_client.get_doors.return_value = [
        door for door in mock_client.get_doors.return_value if door.id != "door-002"
    ]

    # Trigger natural refresh via WebSocket reconnect
    on_disconnect = mock_client.start_websocket.call_args[1]["on_disconnect"]
    on_connect = mock_client.start_websocket.call_args[1]["on_connect"]
    on_disconnect()
    await hass.async_block_till_done()
    on_connect()
    await hass.async_block_till_done()

    # door-001 still exists, door-002 was removed
    assert device_registry.async_get_device(identifiers={(DOMAIN, "door-001")})
    assert not device_registry.async_get_device(identifiers={(DOMAIN, "door-002")})


async def test_stale_device_removed_on_startup(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    mock_config_entry: MockConfigEntry,
    mock_client: MagicMock,
) -> None:
    """Test stale devices present before setup are removed on initial refresh."""
    mock_config_entry.add_to_hass(hass)

    # Create a stale door device that no longer exists on the hub
    device_registry.async_get_or_create(
        config_entry_id=mock_config_entry.entry_id,
        identifiers={(DOMAIN, "door-003")},
    )
    assert device_registry.async_get_device(identifiers={(DOMAIN, "door-003")})

    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    # Valid doors from the hub should exist, stale device should be removed
    assert device_registry.async_get_device(identifiers={(DOMAIN, "door-001")})
    assert device_registry.async_get_device(identifiers={(DOMAIN, "door-002")})
    assert not device_registry.async_get_device(identifiers={(DOMAIN, "door-003")})
