"""Test the UniFi Protect switch platform."""

from unittest.mock import AsyncMock, Mock, call

import pytest
from uiprotect.data import (
    Camera,
    Light,
    Permission,
    PublicHdrMode,
    RecordingMode,
    SmartDetectAudioType,
    SmartDetectObjectType,
    VideoMode,
)
from uiprotect.exceptions import ClientError, NotAuthorized

from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
from homeassistant.components.unifiprotect.switch import (
    ATTR_PREV_MIC,
    ATTR_PREV_RECORD,
    CAMERA_SWITCHES,
    LIGHT_SWITCHES,
    PRIVACY_MODE_SWITCH,
    ProtectSwitchEntityDescription,
)
from homeassistant.const import (
    ATTR_ATTRIBUTION,
    ATTR_ENTITY_ID,
    STATE_OFF,
    STATE_ON,
    STATE_UNAVAILABLE,
    Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er

from . import patch_ufp_method
from .utils import (
    MockUFPFixture,
    adopt_devices,
    assert_entity_counts,
    enable_entity,
    ids_from_device_description,
    init_entry,
    make_public_light,
    public_device_ws_message,
    remove_entities,
    setup_public_light,
)

CAMERA_SWITCHES_BASIC = [
    d
    for d in CAMERA_SWITCHES
    if (
        not d.translation_key.startswith("detections_")
        and d.key not in {"ssh", "color_night_vision", "track_person", "hdr_mode"}
    )
    or d.key
    in {
        "detections_motion",
        "detections_person",
        "detections_vehicle",
        "detections_animal",
    }
]
CAMERA_SWITCHES_NO_EXTRA = [
    d
    for d in CAMERA_SWITCHES_BASIC
    if d.key not in ("high_fps", "privacy_mode", "hdr_mode")
]


async def test_switch_camera_remove(
    hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera
) -> None:
    """Test removing and re-adding a camera device."""

    ufp.api.bootstrap.nvr.system_info.ustorage = None
    await init_entry(hass, ufp, [doorbell, unadopted_camera])
    assert_entity_counts(hass, Platform.SWITCH, 17, 15)
    await remove_entities(hass, ufp, [doorbell, unadopted_camera])
    assert_entity_counts(hass, Platform.SWITCH, 2, 2)
    await adopt_devices(hass, ufp, [doorbell, unadopted_camera])
    assert_entity_counts(hass, Platform.SWITCH, 17, 15)


async def test_switch_light_remove(
    hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
    """Test removing and re-adding a light device."""

    ufp.api.bootstrap.nvr.system_info.ustorage = None
    await init_entry(hass, ufp, [light])
    assert_entity_counts(hass, Platform.SWITCH, 4, 3)
    await remove_entities(hass, ufp, [light])
    assert_entity_counts(hass, Platform.SWITCH, 2, 2)
    await adopt_devices(hass, ufp, [light])
    assert_entity_counts(hass, Platform.SWITCH, 4, 3)


async def test_switch_nvr(hass: HomeAssistant, ufp: MockUFPFixture) -> None:
    """Test switch entity setup for light devices."""

    await init_entry(hass, ufp, [])

    assert_entity_counts(hass, Platform.SWITCH, 2, 2)

    nvr = ufp.api.bootstrap.nvr
    entity_id = "switch.unifiprotect_insights_enabled"

    with patch_ufp_method(nvr, "set_insights", new_callable=AsyncMock) as mock_method:
        await hass.services.async_call(
            "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_called_once_with(True)

        await hass.services.async_call(
            "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_called_with(False)


async def test_switch_setup_no_perm(
    hass: HomeAssistant,
    ufp: MockUFPFixture,
    light: Light,
    doorbell: Camera,
) -> None:
    """Test switch entity setup for light devices."""

    ufp.api.bootstrap.auth_user.all_permissions = [
        Permission.unifi_dict_to_dict({"rawPermission": "light:read:*"})
    ]

    await init_entry(hass, ufp, [light, doorbell])

    assert_entity_counts(hass, Platform.SWITCH, 0, 0)


async def test_switch_setup_light(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    ufp: MockUFPFixture,
    light: Light,
) -> None:
    """Test switch entity setup for light devices."""

    setup_public_light(ufp)
    await init_entry(hass, ufp, [light])
    assert_entity_counts(hass, Platform.SWITCH, 4, 3)

    description = LIGHT_SWITCHES[1]

    unique_id, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, light, description
    )

    entity = entity_registry.async_get(entity_id)
    assert entity
    assert entity.unique_id == unique_id

    state = hass.states.get(entity_id)
    assert state
    assert state.state == STATE_OFF
    assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION

    description = LIGHT_SWITCHES[0]

    unique_id = f"{light.mac}_{description.key}"
    entity_id = f"switch.test_light_{description.translation_key}"

    entity = entity_registry.async_get(entity_id)
    assert entity
    assert entity.disabled is True
    assert entity.unique_id == unique_id

    await enable_entity(hass, ufp.entry.entry_id, entity_id)

    state = hass.states.get(entity_id)
    assert state
    assert state.state == STATE_OFF
    assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION


async def test_switch_setup_camera_all(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    ufp: MockUFPFixture,
    doorbell: Camera,
) -> None:
    """Test switch entity setup for camera devices (all enabled feature flags)."""

    await init_entry(hass, ufp, [doorbell])
    assert_entity_counts(hass, Platform.SWITCH, 17, 15)

    for description in CAMERA_SWITCHES_BASIC:
        unique_id, entity_id = await ids_from_device_description(
            hass, Platform.SWITCH, doorbell, description
        )

        entity = entity_registry.async_get(entity_id)
        assert entity
        assert entity.unique_id == unique_id

        state = hass.states.get(entity_id)
        assert state
        assert state.state == STATE_OFF
        assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION

    description = CAMERA_SWITCHES[0]

    unique_id = f"{doorbell.mac}_{description.key}"
    entity_id = f"switch.test_camera_{description.translation_key}"

    entity = entity_registry.async_get(entity_id)
    assert entity
    assert entity.disabled is True
    assert entity.unique_id == unique_id

    await enable_entity(hass, ufp.entry.entry_id, entity_id)

    state = hass.states.get(entity_id)
    assert state
    assert state.state == STATE_OFF
    assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION


async def test_switch_setup_camera_none(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    ufp: MockUFPFixture,
    camera: Camera,
) -> None:
    """Test switch entity setup for camera devices (no enabled feature flags)."""

    await init_entry(hass, ufp, [camera])
    assert_entity_counts(hass, Platform.SWITCH, 8, 7)

    for description in CAMERA_SWITCHES_BASIC:
        if description.ufp_required_field is not None:
            continue

        unique_id, entity_id = await ids_from_device_description(
            hass, Platform.SWITCH, camera, description
        )

        entity = entity_registry.async_get(entity_id)
        assert entity
        assert entity.unique_id == unique_id

        state = hass.states.get(entity_id)
        assert state
        assert state.state == STATE_OFF
        assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION

    description = CAMERA_SWITCHES[0]

    unique_id = f"{camera.mac}_{description.key}"
    entity_id = f"switch.test_camera_{description.translation_key}"

    entity = entity_registry.async_get(entity_id)
    assert entity
    assert entity.disabled is True
    assert entity.unique_id == unique_id

    await enable_entity(hass, ufp.entry.entry_id, entity_id)

    state = hass.states.get(entity_id)
    assert state
    assert state.state == STATE_OFF
    assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION


async def test_switch_light_status(
    hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
    """Tests status light switch for lights."""

    setup_public_light(ufp)
    await init_entry(hass, ufp, [light])
    assert_entity_counts(hass, Platform.SWITCH, 4, 3)

    description = LIGHT_SWITCHES[1]

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, light, description
    )

    with patch_ufp_method(
        light, "set_status_light_public", new_callable=AsyncMock
    ) as mock_method:
        await hass.services.async_call(
            "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_called_once_with(True)

        await hass.services.async_call(
            "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_called_with(False)


async def test_switch_light_status_public_value(
    hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
    """Status light switch reads from the public object and refreshes on a WS update."""

    setup_public_light(ufp)
    await init_entry(hass, ufp, [light])

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, light, LIGHT_SWITCHES[1]
    )
    assert hass.states.get(entity_id).state == STATE_OFF

    # The private fixture has the indicator disabled; the public ON proves the source.
    public = make_public_light(light, is_indicator_enabled=True)
    ufp.devices_ws_subscription(public_device_ws_message(public))
    await hass.async_block_till_done()

    assert hass.states.get(entity_id).state == STATE_ON


async def test_switch_light_status_unavailable_without_public(
    hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
    """The migrated status light switch is unavailable without a public object."""

    await init_entry(hass, ufp, [light])

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, light, LIGHT_SWITCHES[1]
    )
    assert hass.states.get(entity_id).state == STATE_UNAVAILABLE


async def test_switch_camera_ssh(
    hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
    """Tests SSH switch for cameras."""

    await init_entry(hass, ufp, [doorbell])
    assert_entity_counts(hass, Platform.SWITCH, 17, 15)

    description = CAMERA_SWITCHES[0]

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, doorbell, description
    )
    await enable_entity(hass, ufp.entry.entry_id, entity_id)

    with patch_ufp_method(doorbell, "set_ssh", new_callable=AsyncMock) as mock_method:
        await hass.services.async_call(
            "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_called_once_with(True)

        await hass.services.async_call(
            "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_called_with(False)


@pytest.mark.parametrize("description", CAMERA_SWITCHES_NO_EXTRA)
async def test_switch_camera_simple(
    hass: HomeAssistant,
    ufp: MockUFPFixture,
    doorbell: Camera,
    description: ProtectSwitchEntityDescription,
) -> None:
    """Tests all simple switches for cameras."""

    await init_entry(hass, ufp, [doorbell])
    assert_entity_counts(hass, Platform.SWITCH, 17, 15)

    assert description.ufp_set_method is not None

    with patch_ufp_method(
        doorbell, description.ufp_set_method, new_callable=AsyncMock
    ) as mock_method:
        _, entity_id = await ids_from_device_description(
            hass, Platform.SWITCH, doorbell, description
        )

        await hass.services.async_call(
            "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_called_once_with(True)

        await hass.services.async_call(
            "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_called_with(False)


async def test_switch_camera_highfps(
    hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
    """Tests High FPS switch for cameras."""

    await init_entry(hass, ufp, [doorbell])
    assert_entity_counts(hass, Platform.SWITCH, 17, 15)

    description = CAMERA_SWITCHES[3]

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, doorbell, description
    )

    with patch_ufp_method(
        doorbell, "set_video_mode_public", new_callable=AsyncMock
    ) as mock_method:
        await hass.services.async_call(
            "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_called_once_with(VideoMode.HIGH_FPS)

        await hass.services.async_call(
            "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_called_with(VideoMode.DEFAULT)


CAMERA_SWITCHES_DETECTIONS_EXTRA = [
    d
    for d in CAMERA_SWITCHES
    if d.translation_key.startswith("detections_")
    and d.key
    not in {
        "detections_motion",
        "detections_person",
        "detections_vehicle",
        "detections_animal",
    }
]


async def test_switch_camera_hdr(
    hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
    """Tests HDR mode switch uses the public API helper."""

    await init_entry(hass, ufp, [doorbell])
    assert_entity_counts(hass, Platform.SWITCH, 17, 15)

    description = next(d for d in CAMERA_SWITCHES if d.key == "hdr_mode")

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, doorbell, description
    )
    await enable_entity(hass, ufp.entry.entry_id, entity_id)

    with patch_ufp_method(
        doorbell, "set_hdr_mode_public", new_callable=AsyncMock
    ) as mock_method:
        await hass.services.async_call(
            "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )
        await hass.services.async_call(
            "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_has_calls(
            [call(PublicHdrMode.AUTO), call(PublicHdrMode.OFF)]
        )
        assert mock_method.call_count == 2


@pytest.mark.parametrize("description", CAMERA_SWITCHES_DETECTIONS_EXTRA)
async def test_switch_camera_detections_public_api(
    hass: HomeAssistant,
    ufp: MockUFPFixture,
    doorbell: Camera,
    description: ProtectSwitchEntityDescription,
) -> None:
    """Tests detection switches call the public API setters."""

    doorbell.feature_flags.smart_detect_types = [
        SmartDetectObjectType.PERSON,
        SmartDetectObjectType.VEHICLE,
        SmartDetectObjectType.ANIMAL,
        SmartDetectObjectType.PACKAGE,
        SmartDetectObjectType.LICENSE_PLATE,
    ]
    doorbell.feature_flags.smart_detect_audio_types = [
        SmartDetectAudioType.SMOKE,
        SmartDetectAudioType.CMONX,
        SmartDetectAudioType.SIREN,
        SmartDetectAudioType.BABY_CRY,
        SmartDetectAudioType.SPEAK,
        SmartDetectAudioType.BARK,
        SmartDetectAudioType.BURGLAR,
        SmartDetectAudioType.CAR_HORN,
        SmartDetectAudioType.GLASS_BREAK,
    ]

    await init_entry(hass, ufp, [doorbell])

    assert description.ufp_set_method is not None
    assert description.ufp_set_method.endswith("_public")

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, doorbell, description
    )

    with patch_ufp_method(
        doorbell, description.ufp_set_method, new_callable=AsyncMock
    ) as mock_method:
        await hass.services.async_call(
            "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )
        await hass.services.async_call(
            "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_method.assert_has_calls([call(True), call(False)])
        assert mock_method.call_count == 2


async def test_switch_camera_privacy(
    hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
    """Tests Privacy Mode switch for cameras with privacy mode defaulted on."""

    previous_mic = doorbell.mic_volume = 53
    previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS

    await init_entry(hass, ufp, [doorbell])
    assert_entity_counts(hass, Platform.SWITCH, 17, 15)

    description = PRIVACY_MODE_SWITCH

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, doorbell, description
    )

    state = hass.states.get(entity_id)
    assert state and state.state == "off"
    assert ATTR_PREV_MIC not in state.attributes
    assert ATTR_PREV_RECORD not in state.attributes

    with patch_ufp_method(
        doorbell, "set_privacy", new_callable=AsyncMock
    ) as mock_set_privacy:
        await hass.services.async_call(
            "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_set_privacy.assert_called_with(True, 0, RecordingMode.NEVER)

        new_doorbell = doorbell.model_copy()
        new_doorbell.add_privacy_zone()
        new_doorbell.mic_volume = 0
        new_doorbell.recording_settings.mode = RecordingMode.NEVER
        ufp.api.bootstrap.cameras = {new_doorbell.id: new_doorbell}

        mock_msg = Mock()
        mock_msg.changed_data = {}
        mock_msg.new_obj = new_doorbell
        ufp.ws_msg(mock_msg)

        state = hass.states.get(entity_id)
        assert state and state.state == "on"
        assert state.attributes[ATTR_PREV_MIC] == previous_mic
        assert state.attributes[ATTR_PREV_RECORD] == previous_record.value

        mock_set_privacy.reset_mock()

        await hass.services.async_call(
            "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_set_privacy.assert_called_with(False, previous_mic, previous_record)


async def test_switch_camera_privacy_already_on(
    hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
    """Tests Privacy Mode switch for cameras with privacy mode defaulted on."""

    doorbell.add_privacy_zone()
    await init_entry(hass, ufp, [doorbell])
    assert_entity_counts(hass, Platform.SWITCH, 17, 15)

    description = PRIVACY_MODE_SWITCH

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, doorbell, description
    )

    with patch_ufp_method(
        doorbell, "set_privacy", new_callable=AsyncMock
    ) as mock_set_privacy:
        await hass.services.async_call(
            "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )

        mock_set_privacy.assert_called_once_with(False, 100, RecordingMode.ALWAYS)


async def test_switch_turn_on_client_error(
    hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
    """Test switch turn on with ClientError raises HomeAssistantError."""

    setup_public_light(ufp)
    await init_entry(hass, ufp, [light])

    description = LIGHT_SWITCHES[1]

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, light, description
    )

    with (
        patch_ufp_method(
            light,
            "set_status_light_public",
            new_callable=AsyncMock,
            side_effect=ClientError("Test error"),
        ),
        pytest.raises(HomeAssistantError),
    ):
        await hass.services.async_call(
            "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )


async def test_switch_turn_on_not_authorized(
    hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
    """Test switch turn on with NotAuthorized raises HomeAssistantError."""

    setup_public_light(ufp)
    await init_entry(hass, ufp, [light])

    description = LIGHT_SWITCHES[1]

    _, entity_id = await ids_from_device_description(
        hass, Platform.SWITCH, light, description
    )

    with (
        patch_ufp_method(
            light,
            "set_status_light_public",
            new_callable=AsyncMock,
            side_effect=NotAuthorized("Not authorized"),
        ),
        pytest.raises(HomeAssistantError),
    ):
        await hass.services.async_call(
            "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
        )
