"""Tests for Vizio config flow."""
from __future__ import annotations

from contextlib import asynccontextmanager
from datetime import timedelta
from typing import Any
from unittest.mock import call, patch

import pytest
from pytest import raises
from pyvizio.api.apps import AppConfig
from pyvizio.const import (
    APPS,
    DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER,
    DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV,
    INPUT_APPS,
    MAX_VOLUME,
    UNKNOWN_APP,
)
import voluptuous as vol

from homeassistant.components.media_player import (
    ATTR_INPUT_SOURCE,
    ATTR_MEDIA_VOLUME_LEVEL,
    ATTR_MEDIA_VOLUME_MUTED,
    ATTR_SOUND_MODE,
    DOMAIN as MP_DOMAIN,
    SERVICE_MEDIA_NEXT_TRACK,
    SERVICE_MEDIA_PREVIOUS_TRACK,
    SERVICE_SELECT_SOUND_MODE,
    SERVICE_SELECT_SOURCE,
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    SERVICE_VOLUME_DOWN,
    SERVICE_VOLUME_MUTE,
    SERVICE_VOLUME_SET,
    SERVICE_VOLUME_UP,
    MediaPlayerDeviceClass,
)
from homeassistant.components.media_player.const import ATTR_INPUT_SOURCE_LIST
from homeassistant.components.vizio import validate_apps
from homeassistant.components.vizio.const import (
    CONF_ADDITIONAL_CONFIGS,
    CONF_APPS,
    CONF_VOLUME_STEP,
    DEFAULT_VOLUME_STEP,
    DOMAIN,
    SERVICE_UPDATE_SETTING,
    VIZIO_SCHEMA,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util

from .const import (
    ADDITIONAL_APP_CONFIG,
    APP_LIST,
    APP_NAME_LIST,
    CURRENT_APP,
    CURRENT_APP_CONFIG,
    CURRENT_EQ,
    CURRENT_INPUT,
    CUSTOM_CONFIG,
    ENTITY_ID,
    EQ_LIST,
    INPUT_LIST,
    INPUT_LIST_WITH_APPS,
    MOCK_SPEAKER_APPS_FAILURE,
    MOCK_SPEAKER_CONFIG,
    MOCK_TV_APPS_FAILURE,
    MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG,
    MOCK_TV_WITH_EXCLUDE_CONFIG,
    MOCK_TV_WITH_INCLUDE_CONFIG,
    MOCK_USER_VALID_TV_CONFIG,
    NAME,
    UNIQUE_ID,
    UNKNOWN_APP_CONFIG,
    VOLUME_STEP,
)

from tests.common import MockConfigEntry, async_fire_time_changed


async def _add_config_entry_to_hass(
    hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
    config_entry.add_to_hass(hass)
    assert await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()


def _get_ha_power_state(vizio_power_state: bool | None) -> str:
    """Return HA power state given Vizio power state."""
    if vizio_power_state:
        return STATE_ON

    if vizio_power_state is False:
        return STATE_OFF

    return STATE_UNAVAILABLE


def _assert_sources_and_volume(attr: dict[str, Any], vizio_device_class: str) -> None:
    """Assert source list, source, and volume level based on attr dict and device class."""
    assert attr[ATTR_INPUT_SOURCE_LIST] == INPUT_LIST
    assert attr[ATTR_INPUT_SOURCE] == CURRENT_INPUT
    assert (
        attr["volume_level"]
        == float(int(MAX_VOLUME[vizio_device_class] / 2))
        / MAX_VOLUME[vizio_device_class]
    )


def _get_attr_and_assert_base_attr(
    hass: HomeAssistant, device_class: str, power_state: str
) -> dict[str, Any]:
    """Return entity attributes  after asserting name, device class, and power state."""
    attr = hass.states.get(ENTITY_ID).attributes
    assert attr["friendly_name"] == NAME
    assert attr["device_class"] == device_class

    assert hass.states.get(ENTITY_ID).state == power_state
    return attr


@asynccontextmanager
async def _cm_for_test_setup_without_apps(
    all_settings: dict[str, Any], vizio_power_state: bool | None
) -> None:
    """Context manager to setup test for Vizio devices without including app specific patches."""
    with patch(
        "homeassistant.components.vizio.media_player.VizioAsync.get_all_settings",
        return_value=all_settings,
    ), patch(
        "homeassistant.components.vizio.media_player.VizioAsync.get_setting_options",
        return_value=EQ_LIST,
    ), patch(
        "homeassistant.components.vizio.media_player.VizioAsync.get_power_state",
        return_value=vizio_power_state,
    ):
        yield


async def _test_setup_tv(hass: HomeAssistant, vizio_power_state: bool | None) -> None:
    """Test Vizio TV entity setup."""
    ha_power_state = _get_ha_power_state(vizio_power_state)

    config_entry = MockConfigEntry(
        domain=DOMAIN,
        data=vol.Schema(VIZIO_SCHEMA)(MOCK_USER_VALID_TV_CONFIG),
        unique_id=UNIQUE_ID,
    )

    async with _cm_for_test_setup_without_apps(
        {"volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2), "mute": "Off"},
        vizio_power_state,
    ):
        await _add_config_entry_to_hass(hass, config_entry)

        at
tr = _get_attr_and_assert_base_attr(
            hass, MediaPlayerDeviceClass.TV, ha_power_state
        )
        if ha_power_state == STATE_ON:
            _assert_sources_and_volume(attr, VIZIO_DEVICE_CLASS_TV)
            assert "sound_mode" not in attr


async def _test_setup_speaker(
    hass: HomeAssistant, vizio_power_state: bool | None
) -> None:
    """Test Vizio Speaker entity setup."""
    ha_power_state = _get_ha_power_state(vizio_power_state)

    config_entry = MockConfigEn
try(
        domain=DOMAIN,
        data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG),
        unique_id=UNIQUE_ID,
    )

    audio_settings = {
        "volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_SPEAKER] / 2),
        "mute": "Off",
        "eq": CURRENT_EQ,
    }

    async with _cm_for_test_setup_without_apps(
        audio_settings,
        vizio_power_state,
    ):
        with patch(
            "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config",
        ) as service_call:
            await _add_config_entry_to_hass(hass, config_entry)

            attr = _get_attr_and_assert_base_attr(
                hass, MediaPlayerDeviceClass.SPEAKER, ha_power_state
            )
            if ha_power_state == STATE_ON:
                _assert_sources_and_volume(attr, VIZIO_DEVICE_CLASS_SPEAKER)
                assert not service_call.called
                assert "sound_mode" in attr


@asynccontextmanager
async def _cm_for_test_setup_tv_with_apps(
    hass: HomeAssistant, device_config: dict[str, Any], app_config: dict[str, Any]
) -> None:
    """Context manager to setup test for Vizio TV with support for apps."""
    config_entry = MockConfigEntry(
        domain=DOMAIN, data=vol.Schema(VIZIO_SCHEMA)(device_config), unique_id=UNIQUE_ID
    )

    async with _cm_for_test_setup_without_apps(
        {"volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2), "mute": "Off"},
        True,
    ):
        with patch(
            "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config",
            return_value=AppConfig(**app_config),
        ):
            await _add_config_entry_to_hass(hass, config_entry)

            attr = _get_attr_and_assert_base_attr(
                hass, MediaPlayerDeviceClass.TV, STATE_ON
            )
            assert (
                attr["volume_level"]
                == float(int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2))
                / MAX_VOLUME[VIZIO_DEVICE_CLASS_TV]
            )

            yield


def _assert_source_list_with_apps(
    list_to_test: list[str], attr: dict[str, Any]
) -> None:
    """Assert source list matches list_to_test after removing INPUT_APPS from list."""
    for app_to_remove in INPUT_APPS:
        if app_to_remove in list_to_test:
            list_to_test.remove(app_to_remove)

    assert attr[ATTR_INPUT_SOURCE_LIST] == list_to_test


async def _test_service(
    hass: HomeAssistant,
    domain: str,
    vizio_func_name: str,
    ha_service_name: str,
    additional_service_data: dict[str, Any] | None,
    *args,
    **kwargs,
) -> None:
    """Test generic Vizio media player entity service."""
    kwargs["log_api_exception"] = False
    service_data = {ATTR_ENTITY_ID: ENTITY_ID}
    if additional_service_data:
        service_data.update(additional_service_data)

    with patch(
        f"homeassistant.components.vizio.media_player.VizioAsync.{vizio_func_name}"
    ) as service_call:
        await hass.services.async_c