"""Tests for tedee lock."""

from datetime import timedelta
from unittest.mock import MagicMock, patch
from urllib.parse import urlparse

from aiotedee import TedeeLock, TedeeLockState
from aiotedee.exception import (
    TedeeClientException,
    TedeeDataUpdateException,
    TedeeLocalAuthException,
)
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.lock import (
    DOMAIN as LOCK_DOMAIN,
    SERVICE_LOCK,
    SERVICE_OPEN,
    SERVICE_UNLOCK,
    LockState,
)
from homeassistant.components.webhook import async_generate_url
from homeassistant.const import (
    ATTR_ENTITY_ID,
    STATE_UNAVAILABLE,
    STATE_UNKNOWN,
    Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceNotSupported
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component

from . import setup_integration
from .conftest import WEBHOOK_ID

from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
from tests.typing import ClientSessionGenerator


async def test_locks(
    hass: HomeAssistant,
    mock_tedee: MagicMock,
    mock_config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    snapshot: SnapshotAssertion,
) -> None:
    """Test tedee locks."""
    with patch("homeassistant.components.tedee.PLATFORMS", [Platform.LOCK]):
        await setup_integration(hass, mock_config_entry)

    await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)


@pytest.mark.usefixtures("init_integration")
async def test_lock_service_calls(
    hass: HomeAssistant,
    mock_tedee: MagicMock,
) -> None:
    """Test the tedee lock."""

    await hass.services.async_call(
        LOCK_DOMAIN,
        SERVICE_LOCK,
        {
            ATTR_ENTITY_ID: "lock.lock_1a2b",
        },
        blocking=True,
    )

    assert len(mock_tedee.lock.mock_calls) == 1
    mock_tedee.lock.assert_called_once_with(12345)
    state = hass.states.get("lock.lock_1a2b")
    assert state
    assert state.state == LockState.LOCKING

    await hass.services.async_call(
        LOCK_DOMAIN,
        SERVICE_UNLOCK,
        {
            ATTR_ENTITY_ID: "lock.lock_1a2b",
        },
        blocking=True,
    )

    assert len(mock_tedee.unlock.mock_calls) == 1
    mock_tedee.unlock.assert_called_once_with(12345)
    state = hass.states.get("lock.lock_1a2b")
    assert state
    assert state.state == LockState.UNLOCKING

    await hass.services.async_call(
        LOCK_DOMAIN,
        SERVICE_OPEN,
        {
            ATTR_ENTITY_ID: "lock.lock_1a2b",
        },
        blocking=True,
    )

    assert len(mock_tedee.open.mock_calls) == 1
    mock_tedee.open.assert_called_once_with(12345)
    state = hass.states.get("lock.lock_1a2b")
    assert state
    assert state.state == LockState.UNLOCKING


@pytest.mark.usefixtures("init_integration")
async def test_lock_without_pullspring(
    hass: HomeAssistant,
    mock_tedee: MagicMock,
    device_registry: dr.DeviceRegistry,
    entity_registry: er.EntityRegistry,
    snapshot: SnapshotAssertion,
) -> None:
    """Test the tedee lock without pullspring."""
    # Fetch translations
    await async_setup_component(hass, "homeassistant", {})

    state = hass.states.get("lock.lock_2c3d")
    assert state
    assert state == snapshot

    entry = entity_registry.async_get(state.entity_id)
    assert entry
    assert entry == snapshot

    assert entry.device_id
    device = device_registry.async_get(entry.device_id)
    assert device
    assert device == snapshot

    with pytest.raises(
        ServiceNotSupported,
        match=f"Entity lock.lock_2c3d does not support action {LOCK_DOMAIN}.{SERVICE_OPEN}",
    ):
        await hass.services.async_call(
            LOCK_DOMAIN,
            SERVICE_OPEN,
            {
                ATTR_ENTITY_ID: "lock.lock_2c3d",
            },
            blocking=True,
        )

    assert len(mock_tedee.open.mock_calls) == 0


@pytest.mark.usefixtures("init_integration")
async def test_lock_errors(
    hass: HomeAssistant,
    mock_tedee: MagicMock,
) -> None:
    """Test event errors."""
    mock_tedee.lock.side_effect = TedeeClientException("Boom")
    with pytest.raises(HomeAssistantError) as exc_info:
        await hass.services.async_call(
            LOCK_DOMAIN,
            SERVICE_LOCK,
            {
                ATTR_ENTITY_ID: "lock.lock_1a2b",
            },
            blocking=True,
        )
    assert exc_info.value.translation_key == "lock_failed"

    mock_tedee.unlock.side_effect = TedeeClientException("Boom")
    with pytest.raises(HomeAssistantError) as exc_info:
        await hass.services.async_call(
            LOCK_DOMAIN,
            SERVICE_UNLOCK,
            {
                ATTR_ENTITY_ID: "lock.lock_1a2b",
            },
            blocking=True,
        )
    assert exc_info.value.translation_key == "unlock_failed"

    mock_tedee.open.side_effect = TedeeClientException("Boom")
    with pytest.raises(HomeAssistantError) as exc_info:
        await hass.services.async_call(
            LOCK_DOMAIN,
            SERVICE_OPEN,
            {
                ATTR_ENTITY_ID: "lock.lock_1a2b",
            },
            blocking=True,
        )
    assert exc_info.value.translation_key == "open_failed"


@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize(
    "side_effect",
    [
        TedeeClientException("Boom"),
        TedeeLocalAuthException("Boom"),
        TimeoutError,
        TedeeDataUpdateException("Boom"),
    ],
)
async def test_update_failed(
    hass: HomeAssistant,
    mock_tedee: MagicMock,
    freezer: FrozenDateTimeFactory,
    side_effect: Exception,
) -> None:
    """Test update failed."""
    mock_tedee.sync.side_effect = side_effect
    freezer.tick(timedelta(minutes=10))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    state = hass.states.get("lock.lock_1a2b")
    assert state is not None
    assert state.state == STATE_UNAVAILABLE


@pytest.mark.usefixtures("init_integration")
async def test_cleanup_removed_locks(
    hass: HomeAssistant,
    mock_tedee: MagicMock,
    device_registry: dr.DeviceRegistry,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Ensure removed locks are cleaned up."""

    devices = dr.async_entries_for_config_entry(
        device_registry, mock_config_entry.entry_id
    )

    locks = [device.name for device in devices]
    assert "Lock-1A2B" in locks

    # remove a lock and wait for coordinator
    mock_tedee.locks_dict.pop(12345)
    freezer.tick(timedelta(minutes=10))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    devices = dr.async_entries_for_config_entry(
        device_registry, mock_config_entry.entry_id
    )

    locks = [device.name for device in devices]
    assert "Lock-1A2B" not in locks


@pytest.mark.usefixtures("init_integration")
async def test_new_lock(
    hass: HomeAssistant,
    mock_tedee: MagicMock,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Ensure new lock is added automatically."""

    state = hass.states.get("lock.lock_4e5f")
    assert state is None

    mock_tedee.locks_dict[666666] = TedeeLock("Lock-4E5F", 666666, 2)
    mock_tedee.locks_dict[777777] = TedeeLock(
        "Lock-6G7H",
        777777,
        4,
        is_enabled_pullspring=True,
    )

    freezer.tick(timedelta(minutes=10))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    state = hass.states.get("lock.lock_4e5f")
    assert state
    state = hass.states.get("lock.lock_6g7h")
    assert state


@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize(
    ("lib_state", "expected_state"),
    [
        (TedeeLockState.LOCKED, LockState.LOCKED),
        (TedeeLockState.HALF_OPEN, STATE_UNKNOWN),
        (TedeeLockState.UNKNOWN, STATE_UNKNOWN),
        (TedeeLockState.UNCALIBRATED, STATE_UNAVAILABLE),
    ],
)
async def test_webhook_update(
    hass: HomeAssistant,
    mock_tedee: MagicMock,
    hass_client_no_auth: ClientSessionGenerator,
    lib_state: TedeeLockState,
    expected_state: str,
) -> None:
    """Test updated data set through webhook."""

    state = hass.states.get("lock.lock_1a2b")
    assert state
    assert state.state == LockState.UNLOCKED

    webhook_data = {"dummystate": lib_state.value}
    # is updated in the lib, so mock and assert below
    mock_tedee.locks_dict[12345].state = lib_state
    client = await hass_client_no_auth()
    webhook_url = async_generate_url(hass, WEBHOOK_ID)

    await client.post(
        urlparse(webhook_url).path,
        json=webhook_data,
    )
    mock_tedee.parse_webhook_message.assert_called_once_with(webhook_data)

    state = hass.states.get("lock.lock_1a2b")
    assert state
    assert state.state == expected_state