"""The tests for the tplink camera platform."""

import asyncio
from unittest.mock import AsyncMock, patch

from aiohttp.test_utils import make_mocked_request
from freezegun.api import FrozenDateTimeFactory
from kasa import Module
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components import stream
from homeassistant.components.camera import (
    DOMAIN as CAMERA_DOMAIN,
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    CameraEntityFeature,
    StreamType,
    async_get_image,
    async_get_mjpeg_stream,
    get_camera_from_entity_id,
)
from homeassistant.components.tplink.camera import TPLinkCameraEntity
from homeassistant.components.websocket_api import TYPE_RESULT
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er

from . import _mocked_device, setup_platform_for_device, snapshot_platform
from .const import DEVICE_ID, IP_ADDRESS3, MAC_ADDRESS3, SMALLEST_VALID_JPEG_BYTES

from tests.common import MockConfigEntry, async_fire_time_changed
from tests.typing import WebSocketGenerator


async def test_states(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
    snapshot: SnapshotAssertion,
) -> None:
    """Test camera states."""
    mock_camera_config_entry.add_to_hass(hass)

    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
    )

    # Patch getrandbits so the access_token doesn't change on camera attributes
    with patch("random.SystemRandom.getrandbits", return_value=123123123123):
        await setup_platform_for_device(
            hass, mock_camera_config_entry, Platform.CAMERA, mock_device
        )

    await snapshot_platform(
        hass,
        entity_registry,
        device_registry,
        snapshot,
        mock_camera_config_entry.entry_id,
    )


async def test_camera_unique_id(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test camera unique id."""
    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
        device_id=DEVICE_ID,
    )

    await setup_platform_for_device(
        hass, mock_camera_config_entry, Platform.CAMERA, mock_device
    )

    device_entries = dr.async_entries_for_config_entry(
        device_registry, mock_camera_config_entry.entry_id
    )
    assert device_entries
    entity_id = "camera.my_camera_live_view"

    assert entity_registry.async_get(entity_id).unique_id == f"{DEVICE_ID}-live_view"


async def test_handle_mjpeg_stream(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
) -> None:
    """Test handle_async_mjpeg_stream."""
    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
    )

    await setup_platform_for_device(
        hass, mock_camera_config_entry, Platform.CAMERA, mock_device
    )

    state = hass.states.get("camera.my_camera_live_view")
    assert state is not None

    mock_request = make_mocked_request("GET", "/", headers={"token": "x"})
    stream = await async_get_mjpeg_stream(
        hass, mock_request, "camera.my_camera_live_view"
    )
    assert stream is not None


async def test_handle_mjpeg_stream_not_supported(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
) -> None:
    """Test no stream if stream_rtsp_url is None after creation."""
    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
    )
    mock_camera = mock_device.modules[Module.Camera]

    mock_camera.stream_rtsp_url.side_effect = ("foo", None)

    await setup_platform_for_device(
        hass, mock_camera_config_entry, Platform.CAMERA, mock_device
    )

    mock_request = make_mocked_request("GET", "/", headers={"token": "x"})
    mjpeg_stream = await async_get_mjpeg_stream(
        hass, mock_request, "camera.my_camera_live_view"
    )
    assert mjpeg_stream is None


async def test_camera_image(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test async_get_image."""
    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
    )

    await setup_platform_for_device(
        hass, mock_camera_config_entry, Platform.CAMERA, mock_device
    )

    state = hass.states.get("camera.my_camera_live_view")
    assert state is not None

    with patch(
        "homeassistant.components.ffmpeg.async_get_image",
        return_value=SMALLEST_VALID_JPEG_BYTES,
    ) as mock_get_image:
        image = await async_get_image(hass, "camera.my_camera_live_view")
        assert image
        assert image.content == SMALLEST_VALID_JPEG_BYTES
        mock_get_image.assert_called_once()

        mock_get_image.reset_mock()
        image = await async_get_image(hass, "camera.my_camera_live_view")
        mock_get_image.assert_not_called()

        freezer.tick(TPLinkCameraEntity.IMAGE_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done(wait_background_tasks=True)

        mock_get_image.reset_mock()
        image = await async_get_image(hass, "camera.my_camera_live_view")
        mock_get_image.assert_called_once()

        freezer.tick(TPLinkCameraEntity.IMAGE_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done(wait_background_tasks=True)

    # Test image returns None
    with patch(
        "homeassistant.components.ffmpeg.async_get_image",
        return_value=None,
    ) as mock_get_image:
        msg = f"None camera image returned for {IP_ADDRESS3}"
        assert msg not in caplog.text

        mock_get_image.reset_mock()
        image = await async_get_image(hass, "camera.my_camera_live_view")
        mock_get_image.assert_called_once()

        assert msg in caplog.text


async def test_no_camera_image_when_streaming(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test no camera image when streaming."""
    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
    )

    await setup_platform_for_device(
        hass, mock_camera_config_entry, Platform.CAMERA, mock_device
    )

    state = hass.states.get("camera.my_camera_live_view")
    assert state is not None

    with patch(
        "homeassistant.components.ffmpeg.async_get_image",
        return_value=SMALLEST_VALID_JPEG_BYTES,
    ) as mock_get_image:
        await async_get_image(hass, "camera.my_camera_live_view")
        mock_get_image.assert_called_once()

        freezer.tick(TPLinkCameraEntity.IMAGE_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done(wait_background_tasks=True)

        start_event = asyncio.Event()
        finish_event = asyncio.Event()

        async def _waiter(*_, **__):
            start_event.set()
            await finish_event.wait()

        async def _get_stream():
            mock_request = make_mocked_request("GET", "/", headers={"token": "x"})
            await async_get_mjpeg_stream(
                hass, mock_request, "camera.my_camera_live_view"
            )

        mock_get_image.reset_mock()
        with patch(
            "homeassistant.components.tplink.camera.async_aiohttp_proxy_stream",
            new=_waiter,
        ):
            task = asyncio.create_task(_get_stream())
            await start_event.wait()
            await async_get_image(hass, "camera.my_camera_live_view")
            finish_event.set()
            await task

        mock_get_image.assert_not_called()


async def test_no_concurrent_camera_image(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
) -> None:
    """Test async_get_image doesn't make concurrent requests."""
    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
    )

    await setup_platform_for_device(
        hass, mock_camera_config_entry, Platform.CAMERA, mock_device
    )

    state = hass.states.get("camera.my_camera_live_view")
    assert state is not None

    finish_event = asyncio.Event()
    call_count = 0

    async def _waiter(*_, **__):
        nonlocal call_count
        call_count += 1
        await finish_event.wait()
        return SMALLEST_VALID_JPEG_BYTES

    with patch(
        "homeassistant.components.ffmpeg.async_get_image",
        new=_waiter,
    ):
        tasks = asyncio.gather(
            async_get_image(hass, "camera.my_camera_live_view"),
            async_get_image(hass, "camera.my_camera_live_view"),
        )
        # Sleep to give both tasks chance to get to th asyncio.Lock()
        await asyncio.sleep(0)
        finish_event.set()
        results = await tasks
        assert len(results) == 2
        assert all(img and img.content == SMALLEST_VALID_JPEG_BYTES for img in results)
        assert call_count == 1


async def test_camera_image_auth_error(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
    mock_connect: AsyncMock,
    mock_discovery: AsyncMock,
) -> None:
    """Test async_get_image auth error."""
    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
    )

    await setup_platform_for_device(
        hass, mock_camera_config_entry, Platform.CAMERA, mock_device
    )

    state = hass.states.get("camera.my_camera_live_view")
    assert state is not None
    flows = hass.config_entries.flow.async_progress()
    assert len(flows) == 0

    with (
        patch(
            "homeassistant.components.ffmpeg.async_get_image",
            return_value=b"",
        ),
        patch(
            "homeassistant.components.stream.async_check_stream_client_error",
            side_effect=stream.StreamOpenClientError(
                "Request was unauthorized",
                error_code=stream.StreamClientError.Unauthorized,
            ),
        ),
        pytest.raises(HomeAssistantError),
    ):
        await async_get_image(hass, "camera.my_camera_live_view")
    await hass.async_block_till_done()

    flows = hass.config_entries.flow.async_progress()
    assert len(flows) == 1
    [result] = flows

    assert result["step_id"] == "camera_auth_confirm"


async def test_camera_stream_source(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test camera stream source.

    This test would fail if the integration didn't properly
    put stream in the dependencies.
    """
    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
    )

    await setup_platform_for_device(
        hass, mock_camera_config_entry, Platform.CAMERA, mock_device
    )

    state = hass.states.get("camera.my_camera_live_view")
    assert state is not None

    client = await hass_ws_client(hass)
    await client.send_json_auto_id(
        {"type": "camera/stream", "entity_id": "camera.my_camera_live_view"}
    )
    msg = await client.receive_json()

    # Assert WebSocket response
    assert msg["type"] == TYPE_RESULT
    assert msg["success"]
    assert "url" in msg["result"]


async def test_camera_stream_attributes(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
) -> None:
    """Test stream attributes."""
    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
    )

    await setup_platform_for_device(
        hass, mock_camera_config_entry, Platform.CAMERA, mock_device
    )

    state = hass.states.get("camera.my_camera_live_view")
    assert state is not None

    supported_features = state.attributes.get("supported_features")
    assert supported_features is CameraEntityFeature.STREAM | CameraEntityFeature.ON_OFF
    camera = get_camera_from_entity_id(hass, "camera.my_camera_live_view")
    assert camera.camera_capabilities.frontend_stream_types == {StreamType.HLS}


async def test_camera_turn_on_off(
    hass: HomeAssistant,
    mock_camera_config_entry: MockConfigEntry,
) -> None:
    """Test camera turn on and off."""
    mock_device = _mocked_device(
        modules=[Module.Camera],
        alias="my_camera",
        ip_address=IP_ADDRESS3,
        mac=MAC_ADDRESS3,
    )
    mock_camera = mock_device.modules[Module.Camera]

    await setup_platform_for_device(
        hass, mock_camera_config_entry, Platform.CAMERA, mock_device
    )

    state = hass.states.get("camera.my_camera_live_view")
    assert state is not None

    await hass.services.async_call(
        CAMERA_DOMAIN,
        SERVICE_TURN_ON,
        {"entity_id": "camera.my_camera_live_view"},
        blocking=True,
    )
    mock_camera.set_state.assert_called_with(True)

    await hass.services.async_call(
        CAMERA_DOMAIN,
        SERVICE_TURN_OFF,
        {"entity_id": "camera.my_camera_live_view"},
        blocking=True,
    )
    mock_camera.set_state.assert_called_with(False)
