"""Test the motionEye camera."""

from collections.abc import Callable
import copy
from unittest.mock import AsyncMock, Mock, call

from aiohttp import web
from aiohttp.test_utils import TestServer
from aiohttp.web_exceptions import HTTPBadGateway
from motioneye_client.client import (
    MotionEyeClientError,
    MotionEyeClientInvalidAuthError,
    MotionEyeClientURLParseError,
)
from motioneye_client.const import (
    KEY_CAMERAS,
    KEY_MOTION_DETECTION,
    KEY_NAME,
    KEY_TEXT_OVERLAY_CUSTOM_TEXT,
    KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT,
    KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT,
    KEY_TEXT_OVERLAY_LEFT,
    KEY_TEXT_OVERLAY_RIGHT,
    KEY_TEXT_OVERLAY_TIMESTAMP,
    KEY_VIDEO_STREAMING,
)
import pytest
import voluptuous as vol

from homeassistant.components.camera import async_get_image, async_get_mjpeg_stream
from homeassistant.components.motioneye import get_motioneye_device_identifier
from homeassistant.components.motioneye.const import (
    CONF_ACTION,
    CONF_STREAM_URL_TEMPLATE,
    CONF_SURVEILLANCE_USERNAME,
    DEFAULT_SCAN_INTERVAL,
    DOMAIN,
    MOTIONEYE_MANUFACTURER,
    SERVICE_ACTION,
    SERVICE_SET_TEXT_OVERLAY,
    SERVICE_SNAPSHOT,
)
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util import dt as dt_util
from homeassistant.util.aiohttp import MockRequest

from . import (
    TEST_CAMERA,
    TEST_CAMERA_DEVICE_IDENTIFIER,
    TEST_CAMERA_ENTITY_ID,
    TEST_CAMERA_ID,
    TEST_CAMERA_NAME,
    TEST_CAMERAS,
    TEST_CONFIG_ENTRY_ID,
    TEST_SURVEILLANCE_USERNAME,
    create_mock_motioneye_client,
    create_mock_motioneye_config_entry,
    setup_mock_motioneye_config_entry,
)

from tests.common import async_fire_time_changed


@pytest.fixture
def aiohttp_server(
    aiohttp_server: Callable[[], TestServer],
    socket_enabled: None,
) -> Callable[[], TestServer]:
    """Return aiohttp_server and allow opening sockets."""
    return aiohttp_server


async def test_setup_camera(hass: HomeAssistant) -> None:
    """Test a basic camera."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID)
    assert entity_state
    assert entity_state.state == "streaming"
    assert entity_state.attributes.get("friendly_name") == TEST_CAMERA_NAME


async def test_setup_camera_auth_fail(hass: HomeAssistant) -> None:
    """Test a successful camera."""
    client = create_mock_motioneye_client()
    client.async_client_login = AsyncMock(side_effect=MotionEyeClientInvalidAuthError)
    await setup_mock_motioneye_config_entry(hass, client=client)
    assert not hass.states.get(TEST_CAMERA_ENTITY_ID)


async def test_setup_camera_client_error(hass: HomeAssistant) -> None:
    """Test a successful camera."""
    client = create_mock_motioneye_client()
    client.async_client_login = AsyncMock(side_effect=MotionEyeClientError)
    await setup_mock_motioneye_config_entry(hass, client=client)
    assert not hass.states.get(TEST_CAMERA_ENTITY_ID)


async def test_setup_camera_empty_data(hass: HomeAssistant) -> None:
    """Test a successful camera."""
    client = create_mock_motioneye_client()
    client.async_get_cameras = AsyncMock(return_value={})
    await setup_mock_motioneye_config_entry(hass, client=client)
    assert not hass.states.get(TEST_CAMERA_ENTITY_ID)


async def test_setup_camera_bad_data(hass: HomeAssistant) -> None:
    """Test bad camera data."""
    client = create_mock_motioneye_client()
    cameras = copy.deepcopy(TEST_CAMERAS)
    del cameras[KEY_CAMERAS][0][KEY_NAME]

    client.async_get_cameras = AsyncMock(return_value=cameras)
    await setup_mock_motioneye_config_entry(hass, client=client)
    assert not hass.states.get(TEST_CAMERA_ENTITY_ID)


async def test_setup_camera_without_streaming(hass: HomeAssistant) -> None:
    """Test a camera without streaming enabled."""
    client = create_mock_motioneye_client()
    cameras = copy.deepcopy(TEST_CAMERAS)
    cameras[KEY_CAMERAS][0][KEY_VIDEO_STREAMING] = False

    client.async_get_cameras = AsyncMock(return_value=cameras)
    await setup_mock_motioneye_config_entry(hass, client=client)
    entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID)
    assert entity_state
    assert entity_state.state == "unavailable"


async def test_setup_camera_new_data_same(hass: HomeAssistant) -> None:
    """Test a data refresh with the same data."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)
    async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL)
    await hass.async_block_till_done()
    assert hass.states.get(TEST_CAMERA_ENTITY_ID)


async def test_setup_camera_new_data_camera_removed(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test a data refresh with a removed camera."""

    client = create_mock_motioneye_client()
    config_entry = await setup_mock_motioneye_config_entry(hass, client=client)

    # Create some random old devices/entity_ids and ensure they get cleaned up.
    old_device_id = "old-device-id"
    old_entity_unique_id = "old-entity-unique_id"
    old_device = device_registry.async_get_or_create(
        config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, old_device_id)}
    )
    entity_registry.async_get_or_create(
        domain=DOMAIN,
        platform="camera",
        unique_id=old_entity_unique_id,
        config_entry=config_entry,
        device_id=old_device.id,
    )

    await hass.async_block_till_done()
    assert hass.states.get(TEST_CAMERA_ENTITY_ID)
    assert device_registry.async_get_device(identifiers={TEST_CAMERA_DEVICE_IDENTIFIER})

    client.async_get_cameras = AsyncMock(return_value={KEY_CAMERAS: []})
    async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL)
    await hass.async_block_till_done()
    await hass.async_block_till_done()
    assert not hass.states.get(TEST_CAMERA_ENTITY_ID)
    assert not device_registry.async_get_device(
        identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}
    )
    assert not device_registry.async_get_device(identifiers={(DOMAIN, old_device_id)})
    assert not entity_registry.async_get_entity_id(
        DOMAIN, "camera", old_entity_unique_id
    )


async def test_setup_camera_new_data_error(hass: HomeAssistant) -> None:
    """Test a data refresh that fails."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)
    assert hass.states.get(TEST_CAMERA_ENTITY_ID)
    client.async_get_cameras = AsyncMock(side_effect=MotionEyeClientError)
    async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL)
    await hass.async_block_till_done()
    entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID)
    assert entity_state
    assert entity_state.state == "unavailable"


async def test_setup_camera_new_data_without_streaming(hass: HomeAssistant) -> None:
    """Test a data refresh without streaming."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)
    entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID)
    assert entity_state
    assert entity_state.state == "streaming"

    cameras = copy.deepcopy(TEST_CAMERAS)
    cameras[KEY_CAMERAS][0][KEY_VIDEO_STREAMING] = False
    client.async_get_cameras = AsyncMock(return_value=cameras)
    async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL)
    await hass.async_block_till_done()
    entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID)
    assert entity_state
    assert entity_state.state == "unavailable"


async def test_unload_camera(hass: HomeAssistant) -> None:
    """Test unloading camera."""
    client = create_mock_motioneye_client()
    entry = await setup_mock_motioneye_config_entry(hass, client=client)
    assert hass.states.get(TEST_CAMERA_ENTITY_ID)
    assert not client.async_client_close.called
    await hass.config_entries.async_unload(entry.entry_id)
    assert client.async_client_close.called


async def test_get_still_image_from_camera(
    aiohttp_server: Callable[[], TestServer], hass: HomeAssistant
) -> None:
    """Test getting a still image."""

    image_handler = AsyncMock(return_value=web.Response(body=""))

    app = web.Application()
    app.add_routes(
        [
            web.get(
                "/foo",
                image_handler,
            )
        ]
    )

    server = await aiohttp_server(app)
    client = create_mock_motioneye_client()
    client.get_camera_snapshot_url = Mock(
        return_value=f"http://127.0.0.1:{server.port}/foo"
    )
    config_entry = create_mock_motioneye_config_entry(
        hass,
        data={
            CONF_URL: f"http://127.0.0.1:{server.port}",
            CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME,
        },
    )

    await setup_mock_motioneye_config_entry(
        hass, config_entry=config_entry, client=client
    )
    await hass.async_block_till_done()

    # It won't actually get a stream from the dummy handler, so just catch
    # the expected exception, then verify the right handler was called.
    with pytest.raises(HomeAssistantError):
        await async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=1)
    assert image_handler.called


async def test_get_stream_from_camera(
    aiohttp_server: Callable[[], TestServer], hass: HomeAssistant
) -> None:
    """Test getting a stream."""

    stream_handler = AsyncMock(return_value=web.Response(body=""))

    app = web.Application()
    app.add_routes([web.get("/", stream_handler)])
    stream_server = await aiohttp_server(app)

    client = create_mock_motioneye_client()
    client.get_camera_stream_url = Mock(
        return_value=f"http://127.0.0.1:{stream_server.port}/"
    )
    config_entry = create_mock_motioneye_config_entry(
        hass,
        data={
            CONF_URL: f"http://127.0.0.1:{stream_server.port}",
            # The port won't be used as the client is a mock.
            CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME,
        },
    )
    cameras = copy.deepcopy(TEST_CAMERAS)
    client.async_get_cameras = AsyncMock(return_value=cameras)
    await setup_mock_motioneye_config_entry(
        hass, config_entry=config_entry, client=client
    )
    await hass.async_block_till_done()

    await async_get_mjpeg_stream(hass, MockRequest(b"", "test"), TEST_CAMERA_ENTITY_ID)
    assert stream_handler.called


async def test_state_attributes(hass: HomeAssistant) -> None:
    """Test state attributes are set correctly."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID)
    assert entity_state
    assert entity_state.attributes.get("brand") == MOTIONEYE_MANUFACTURER
    assert entity_state.attributes.get("motion_detection")

    cameras = copy.deepcopy(TEST_CAMERAS)
    cameras[KEY_CAMERAS][0][KEY_MOTION_DETECTION] = False
    client.async_get_cameras = AsyncMock(return_value=cameras)
    async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL)
    await hass.async_block_till_done()

    entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID)
    assert entity_state
    assert not entity_state.attributes.get("motion_detection")


async def test_device_info(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    entity_registry: er.EntityRegistry,
) -> None:
    """Verify device information includes expected details."""
    entry = await setup_mock_motioneye_config_entry(hass)

    device_identifier = get_motioneye_device_identifier(entry.entry_id, TEST_CAMERA_ID)

    device = device_registry.async_get_device(identifiers={device_identifier})
    assert device
    assert device.config_entries == {TEST_CONFIG_ENTRY_ID}
    assert device.identifiers == {device_identifier}
    assert device.manufacturer == MOTIONEYE_MANUFACTURER
    assert device.model == MOTIONEYE_MANUFACTURER
    assert device.name == TEST_CAMERA_NAME

    entities_from_device = [
        entry.entity_id
        for entry in er.async_entries_for_device(entity_registry, device.id)
    ]
    assert TEST_CAMERA_ENTITY_ID in entities_from_device


async def test_camera_option_stream_url_template(
    aiohttp_server: Callable[[], TestServer], hass: HomeAssistant
) -> None:
    """Verify camera with a stream URL template option."""
    client = create_mock_motioneye_client()

    stream_handler = AsyncMock(return_value=web.Response(body=""))

    app = web.Application()
    app.add_routes([web.get(f"/{TEST_CAMERA_NAME}/{TEST_CAMERA_ID}", stream_handler)])
    stream_server = await aiohttp_server(app)

    client = create_mock_motioneye_client()

    config_entry = create_mock_motioneye_config_entry(
        hass,
        data={
            CONF_URL: f"http://127.0.0.1:{stream_server.port}",
            # The port won't be used as the client is a mock.
            CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME,
        },
        options={
            CONF_STREAM_URL_TEMPLATE: (
                f"http://127.0.0.1:{stream_server.port}/{{{{ name }}}}/{{{{ id }}}}"
            )
        },
    )

    await setup_mock_motioneye_config_entry(
        hass, config_entry=config_entry, client=client
    )
    await hass.async_block_till_done()

    await async_get_mjpeg_stream(hass, MockRequest(b"", "test"), TEST_CAMERA_ENTITY_ID)
    assert AsyncMock.called
    assert not client.get_camera_stream_url.called


async def test_get_stream_from_camera_with_broken_host(
    aiohttp_server: Callable[[], TestServer], hass: HomeAssistant
) -> None:
    """Test getting a stream with a broken URL (no host)."""

    client = create_mock_motioneye_client()
    config_entry = create_mock_motioneye_config_entry(hass, data={CONF_URL: "http://"})
    client.get_camera_stream_url = Mock(side_effect=MotionEyeClientURLParseError)

    await setup_mock_motioneye_config_entry(
        hass, config_entry=config_entry, client=client
    )
    await hass.async_block_till_done()
    with pytest.raises(HTTPBadGateway):
        await async_get_mjpeg_stream(hass, Mock(), TEST_CAMERA_ENTITY_ID)


async def test_set_text_overlay_bad_extra_key(hass: HomeAssistant) -> None:
    """Test text overlay with incorrect input data."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    data = {ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID, "extra_key": "foo"}
    with pytest.raises(vol.error.MultipleInvalid):
        await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data)


async def test_set_text_overlay_bad_entity_identifier(hass: HomeAssistant) -> None:
    """Test text overlay with bad entity identifier."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    data = {
        ATTR_ENTITY_ID: "some random string",
        KEY_TEXT_OVERLAY_LEFT: KEY_TEXT_OVERLAY_TIMESTAMP,
    }

    client.reset_mock()
    with pytest.raises(vol.error.MultipleInvalid):
        await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data)


async def test_set_text_overlay_bad_empty(hass: HomeAssistant) -> None:
    """Test text overlay with incorrect input data."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)
    with pytest.raises(vol.error.MultipleInvalid):
        await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, {})


async def test_set_text_overlay_bad_no_left_or_right(hass: HomeAssistant) -> None:
    """Test text overlay with incorrect input data."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    data = {ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID}
    with pytest.raises(vol.error.MultipleInvalid):
        await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data)


async def test_set_text_overlay_good(hass: HomeAssistant) -> None:
    """Test a working text overlay."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    custom_left_text = "one\ntwo\nthree"
    custom_right_text = "four\nfive\nsix"
    data = {
        ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID,
        KEY_TEXT_OVERLAY_LEFT: KEY_TEXT_OVERLAY_CUSTOM_TEXT,
        KEY_TEXT_OVERLAY_RIGHT: KEY_TEXT_OVERLAY_CUSTOM_TEXT,
        KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT: custom_left_text,
        KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT: custom_right_text,
    }
    client.async_get_camera = AsyncMock(return_value=copy.deepcopy(TEST_CAMERA))

    await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data)
    await hass.async_block_till_done()
    assert client.async_get_camera.called

    expected_camera = copy.deepcopy(TEST_CAMERA)
    expected_camera[KEY_TEXT_OVERLAY_LEFT] = KEY_TEXT_OVERLAY_CUSTOM_TEXT
    expected_camera[KEY_TEXT_OVERLAY_RIGHT] = KEY_TEXT_OVERLAY_CUSTOM_TEXT
    expected_camera[KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT] = "one\\ntwo\\nthree"
    expected_camera[KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT] = "four\\nfive\\nsix"
    assert client.async_set_camera.call_args == call(TEST_CAMERA_ID, expected_camera)


async def test_set_text_overlay_good_entity_id(hass: HomeAssistant) -> None:
    """Test a working text overlay with entity_id."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    data = {
        ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID,
        KEY_TEXT_OVERLAY_LEFT: KEY_TEXT_OVERLAY_TIMESTAMP,
    }
    client.async_get_camera = AsyncMock(return_value=copy.deepcopy(TEST_CAMERA))
    await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data)
    await hass.async_block_till_done()
    assert client.async_get_camera.called

    expected_camera = copy.deepcopy(TEST_CAMERA)
    expected_camera[KEY_TEXT_OVERLAY_LEFT] = KEY_TEXT_OVERLAY_TIMESTAMP
    assert client.async_set_camera.call_args == call(TEST_CAMERA_ID, expected_camera)


async def test_set_text_overlay_bad_device(hass: HomeAssistant) -> None:
    """Test a working text overlay."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    data = {
        ATTR_DEVICE_ID: "not a device",
        KEY_TEXT_OVERLAY_LEFT: KEY_TEXT_OVERLAY_TIMESTAMP,
    }
    client.reset_mock()
    client.async_get_camera = AsyncMock(return_value=copy.deepcopy(TEST_CAMERA))
    await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data)
    await hass.async_block_till_done()
    assert not client.async_get_camera.called
    assert not client.async_set_camera.called


async def test_set_text_overlay_no_such_camera(hass: HomeAssistant) -> None:
    """Test a working text overlay."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    data = {
        ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID,
        KEY_TEXT_OVERLAY_LEFT: KEY_TEXT_OVERLAY_TIMESTAMP,
    }
    client.reset_mock()
    client.async_get_camera = AsyncMock(return_value={})
    await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data)
    await hass.async_block_till_done()
    assert not client.async_set_camera.called


async def test_request_action(hass: HomeAssistant) -> None:
    """Test requesting an action."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    data = {
        ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID,
        CONF_ACTION: "foo",
    }
    await hass.services.async_call(DOMAIN, SERVICE_ACTION, data)
    await hass.async_block_till_done()
    assert client.async_action.call_args == call(TEST_CAMERA_ID, data[CONF_ACTION])


async def test_request_snapshot(hass: HomeAssistant) -> None:
    """Test requesting a snapshot."""
    client = create_mock_motioneye_client()
    await setup_mock_motioneye_config_entry(hass, client=client)

    data = {ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID}

    await hass.services.async_call(DOMAIN, SERVICE_SNAPSHOT, data)
    await hass.async_block_till_done()
    assert client.async_action.call_args == call(TEST_CAMERA_ID, "snapshot")
