"""Fixtures for component testing."""

import asyncio
from collections.abc import AsyncGenerator, Callable, Coroutine, Generator, Mapping
from functools import lru_cache
from importlib.util import find_spec
import inspect
from ipaddress import IPv4Address, IPv4Network
from pathlib import Path
import re
import string
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, MagicMock, patch

from aiohasupervisor import SupervisorClient, SupervisorNotFoundError
from aiohasupervisor.addons import AddonsClient
from aiohasupervisor.backups import BackupsClient
from aiohasupervisor.discovery import DiscoveryClient
from aiohasupervisor.homeassistant import HomeAssistantClient
from aiohasupervisor.host import HostClient
from aiohasupervisor.ingress import IngressClient
from aiohasupervisor.jobs import JobsClient
from aiohasupervisor.models import (
    AddonStage,
    AddonState,
    Discovery,
    DockerNetwork,
    GreenInfo,
    HomeAssistantInfo,
    HomeAssistantStats,
    HostInfo,
    InstalledAddon,
    JobsInfo,
    LogLevel,
    MountsInfo,
    NetworkInfo,
    OSInfo,
    Repository,
    ResolutionInfo,
    RootInfo,
    StoreAddon,
    StoreInfo,
    SupervisorInfo,
    SupervisorState,
    SupervisorStats,
    UpdateChannel,
    YellowInfo,
)
from aiohasupervisor.mounts import MountsClient
from aiohasupervisor.network import NetworkClient
from aiohasupervisor.os import OSClient
from aiohasupervisor.resolution import ResolutionClient
from aiohasupervisor.store import StoreClient
from aiohasupervisor.supervisor import SupervisorManagementClient
import pytest
import voluptuous as vol

from homeassistant import components, loader
from homeassistant.components import repairs
from homeassistant.config_entries import (
    DISCOVERY_SOURCES,
    ConfigEntriesFlowManager,
    FlowResult,
    OptionsFlowManager,
)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import (
    Context,
    EntityServiceResponse,
    HassJobType,
    HomeAssistant,
    ServiceCall,
    ServiceRegistry,
    ServiceResponse,
    SupportsResponse,
    callback,
)
from homeassistant.data_entry_flow import (
    FlowContext,
    FlowHandler,
    FlowManager,
    FlowResultType,
    section,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.translation import async_get_translations
from homeassistant.helpers.typing import VolSchemaType
from homeassistant.util import yaml as yaml_util

from tests.common import QualityScaleStatus, get_quality_scale

if TYPE_CHECKING:
    from homeassistant.components.hassio import AddonManager

    from .conversation import MockAgent
    from .device_tracker.common import MockScanner
    from .infrared.common import MockInfraredEmitterEntity, MockInfraredReceiverEntity
    from .light.common import MockLight
    from .radio_frequency.common import MockRadioFrequencyEntity
    from .sensor.common import MockSensor
    from .switch.common import MockSwitch

pytest.register_assert_rewrite("tests.components.common")

# Regex for accessing the integration name from the test path
RE_REQUEST_DOMAIN = re.compile(r".*tests\/components\/([^/]+)\/.*")


@pytest.fixture(scope="session", autouse=find_spec("zeroconf") is not None)
def patch_zeroconf_multiple_catcher() -> Generator[None]:
    """Patch zeroconf wrapper that detects multiple instances."""
    with patch(
        "homeassistant.components.zeroconf.install_multiple_zeroconf_catcher",
        side_effect=lambda zc: None,
    ):
        yield


@pytest.fixture(scope="session", autouse=True)
def prevent_io() -> Generator[None]:
    """Fixture to prevent certain I/O from happening."""
    with patch(
        "homeassistant.components.http.ban.load_yaml_config_file",
    ):
        yield


@pytest.fixture
def entity_registry_enabled_by_default() -> Generator[None]:
    """Test fixture that ensures all entities are enabled in the registry."""
    with (
        patch(
            "homeassistant.helpers.entity.Entity.entity_registry_enabled_default",
            return_value=True,
        ),
        patch(
            "homeassistant.components.device_tracker.config_entry.ScannerEntity.entity_registry_enabled_default",
            return_value=True,
        ),
    ):
        yield


# TTS test fixtures
@pytest.fixture(name="mock_tts_get_cache_files")
def mock_tts_get_cache_files_fixture() -> Generator[MagicMock]:
    """Mock the list TTS cache function."""
    from .tts.common import mock_tts_get_cache_files_fixture_helper  # noqa: PLC0415

    yield from mock_tts_get_cache_files_fixture_helper()


@pytest.fixture(name="mock_tts_init_cache_dir")
def mock_tts_init_cache_dir_fixture(
    init_tts_cache_dir_side_effect: Any,
) -> Generator[MagicMock]:
    """Mock the TTS cache dir in memory."""
    from .tts.common import mock_tts_init_cache_dir_fixture_helper  # noqa: PLC0415

    yield from mock_tts_init_cache_dir_fixture_helper(init_tts_cache_dir_side_effect)


@pytest.fixture(name="init_tts_cache_dir_side_effect")
def init_tts_cache_dir_side_effect_fixture() -> Any:
    """Return the cache dir."""
    from .tts.common import (  # noqa: PLC0415
        init_tts_cache_dir_side_effect_fixture_helper,
    )

    return init_tts_cache_dir_side_effect_fixture_helper()


@pytest.fixture(name="mock_tts_cache_dir")
def mock_tts_cache_dir_fixture(
    tmp_path: Path,
    mock_tts_init_cache_dir: MagicMock,
    mock_tts_get_cache_files: MagicMock,
    request: pytest.FixtureRequest,
) -> Generator[Path]:
    """Mock the TTS cache dir with empty dir."""
    from .tts.common import mock_tts_cache_dir_fixture_helper  # noqa: PLC0415

    yield from mock_tts_cache_dir_fixture_helper(
        tmp_path, mock_tts_init_cache_dir, mock_tts_get_cache_files, request
    )


@pytest.fixture(name="tts_mutagen_mock")
def tts_mutagen_mock_fixture() -> Generator[MagicMock]:
    """Mock writing tags."""
    from .tts.common import tts_mutagen_mock_fixture_helper  # noqa: PLC0415

    yield from tts_mutagen_mock_fixture_helper()


@pytest.fixture(name="mock_conversation_agent")
def mock_conversation_agent_fixture(hass: HomeAssistant) -> MockAgent:
    """Mock a conversation agent."""
    from .conversation.common import (  # noqa: PLC0415
        mock_conversation_agent_fixture_helper,
    )

    return mock_conversation_agent_fixture_helper(hass)


# Radio frequency test fixtures
@pytest.fixture(name="init_radio_frequency")
async def init_radio_frequency_fixture(hass: HomeAssistant) -> None:
    """Set up the Radio Frequency integration for testing."""
    from .radio_frequency.common import (  # noqa: PLC0415
        init_radio_frequency_fixture_helper,
    )

    await init_radio_frequency_fixture_helper(hass)


@pytest.fixture(name="mock_rf_entity")
async def mock_rf_entity_fixture(
    hass: HomeAssistant, init_radio_frequency: None
) -> MockRadioFrequencyEntity:
    """Return a mock radio frequency entity."""
    from .radio_frequency.common import mock_rf_entity_fixture_helper  # noqa: PLC0415

    return await mock_rf_entity_fixture_helper(hass)


# Infrared test fixtures
@pytest.fixture(name="init_infrared")
async def init_infrared_fixture(hass: HomeAssistant) -> None:
    """Set up the Infrared integration for testing."""
    from .infrared.common import init_infrared_fixture_helper  # noqa: PLC0415

    await init_infrared_fixture_helper(hass)


@pytest.fixture(name="mock_infrared_emitter_entity")
async def mock_infrared_emitter_entity_fixture(
    hass: HomeAssistant, init_infrared: None
) -> MockInfraredEmitterEntity:
    """Return a mock infrared emitter entity."""
    from .infrared.common import (  # noqa: PLC0415
        mock_infrared_emitter_entity_fixture_helper,
    )

    return await mock_infrared_emitter_entity_fixture_helper(hass)


@pytest.fixture(name="mock_infrared_receiver_entity")
async def mock_infrared_receiver_entity_fixture(
    hass: HomeAssistant, init_infrared: None
) -> MockInfraredReceiverEntity:
    """Return a mock infrared receiver entity."""
    from .infrared.common import (  # noqa: PLC0415
        mock_infrared_receiver_entity_fixture_helper,
    )

    return await mock_infrared_receiver_entity_fixture_helper(hass)


@pytest.fixture(scope="session", autouse=find_spec("haffmpeg") is not None)
def prevent_ffmpeg_subprocess() -> Generator[None]:
    """If installed, prevent ffmpeg from creating a subprocess."""
    with patch(
        "homeassistant.components.ffmpeg.FFVersion.get_version", return_value="6.0"
    ):
        yield


@pytest.fixture
def mock_light_entities() -> list[MockLight]:
    """Return mocked light entities."""
    from .light.common import MockLight  # noqa: PLC0415

    return [
        MockLight("Ceiling", STATE_ON),
        MockLight("Ceiling", STATE_OFF),
        MockLight(None, STATE_OFF),
    ]


@pytest.fixture
def mock_sensor_entities() -> dict[str, MockSensor]:
    """Return mocked sensor entities."""
    from .sensor.common import get_mock_sensor_entities  # noqa: PLC0415

    return get_mock_sensor_entities()


@pytest.fixture
def mock_switch_entities() -> list[MockSwitch]:
    """Return mocked toggle entities."""
    from .switch.common import get_mock_switch_entities  # noqa: PLC0415

    return get_mock_switch_entities()


@pytest.fixture
def mock_legacy_device_scanner() -> MockScanner:
    """Return mocked legacy device scanner entity."""
    from .device_tracker.common import MockScanner  # noqa: PLC0415

    return MockScanner()


@pytest.fixture
def mock_legacy_device_tracker_setup() -> Callable[[HomeAssistant, MockScanner], None]:
    """Return setup callable for legacy device tracker setup."""
    from .device_tracker.common import mock_legacy_device_tracker_setup  # noqa: PLC0415

    return mock_legacy_device_tracker_setup


@pytest.fixture(name="addon_manager")
def addon_manager_fixture(
    hass: HomeAssistant, supervisor_client: AsyncMock
) -> AddonManager:
    """Return an AddonManager instance."""
    from .hassio.common import mock_addon_manager  # noqa: PLC0415

    return mock_addon_manager(hass)


@pytest.fixture(name="discovery_info")
def discovery_info_fixture() -> list[Discovery]:
    """Return the discovery info from the supervisor."""
    return []


@pytest.fixture(name="discovery_info_side_effect")
def discovery_info_side_effect_fixture() -> Any | None:
    """Return the discovery info from the supervisor."""
    return None


@pytest.fixture(name="get_addon_discovery_info")
def get_addon_discovery_info_fixture(
    supervisor_client: AsyncMock,
    discovery_info: list[Discovery],
    discovery_info_side_effect: Any | None,
) -> AsyncMock:
    """Mock get add-on discovery info."""
    supervisor_client.discovery.list.return_value = discovery_info
    supervisor_client.discovery.list.side_effect = discovery_info_side_effect
    return supervisor_client.discovery.list


@pytest.fixture(name="get_discovery_message_side_effect")
def get_discovery_message_side_effect_fixture() -> Any | None:
    """Side effect for getting a discovery message by uuid."""
    return None


@pytest.fixture(name="get_discovery_message")
def get_discovery_message_fixture(
    supervisor_client: AsyncMock, get_discovery_message_side_effect: Any | None
) -> AsyncMock:
    """Mock getting a discovery message by uuid."""
    supervisor_client.discovery.get.side_effect = get_discovery_message_side_effect
    return supervisor_client.discovery.get


@pytest.fixture(name="addon_store_info_side_effect")
def addon_store_info_side_effect_fixture() -> Any | None:
    """Return the add-on store info side effect."""
    return None


@pytest.fixture(name="addon_store_info")
def addon_store_info_fixture(
    supervisor_client: AsyncMock,
    addon_store_info_side_effect: Any | None,
) -> AsyncMock:
    """Mock Supervisor add-on store info."""
    from .hassio.common import mock_addon_store_info  # noqa: PLC0415

    return mock_addon_store_info(supervisor_client, addon_store_info_side_effect)


@pytest.fixture(name="addon_info_side_effect")
def addon_info_side_effect_fixture() -> Any | None:
    """Return the add-on info side effect."""
    return None


@pytest.fixture(name="addon_info")
def addon_info_fixture(
    supervisor_client: AsyncMock, addon_info_side_effect: Any | None
) -> AsyncMock:
    """Mock Supervisor add-on info."""
    from .hassio.common import mock_addon_info  # noqa: PLC0415

    return mock_addon_info(supervisor_client, addon_info_side_effect)


@pytest.fixture(name="addon_not_installed")
def addon_not_installed_fixture(
    addon_store_info: AsyncMock, addon_info: AsyncMock
) -> AsyncMock:
    """Mock add-on not installed."""
    from .hassio.common import mock_addon_not_installed  # noqa: PLC0415

    addon_info.side_effect = SupervisorNotFoundError
    return mock_addon_not_installed(addon_store_info, addon_info)


@pytest.fixture(name="addon_installed")
def addon_installed_fixture(
    addon_store_info: AsyncMock, addon_info: AsyncMock
) -> AsyncMock:
    """Mock add-on already installed but not running."""
    from .hassio.common import mock_addon_installed  # noqa: PLC0415

    return mock_addon_installed(addon_store_info, addon_info)


@pytest.fixture(name="addon_running")
def addon_running_fixture(
    addon_store_info: AsyncMock, addon_info: AsyncMock
) -> AsyncMock:
    """Mock add-on already running."""
    from .hassio.common import mock_addon_running  # noqa: PLC0415

    return mock_addon_running(addon_store_info, addon_info)


@pytest.fixture(name="install_addon_side_effect")
def install_addon_side_effect_fixture(
    addon_store_info: AsyncMock, addon_info: AsyncMock
) -> Any | None:
    """Return the install add-on side effect."""

    from .hassio.common import mock_install_addon_side_effect  # noqa: PLC0415

    return mock_install_addon_side_effect(addon_store_info, addon_info)


@pytest.fixture(name="install_addon")
def install_addon_fixture(
    supervisor_client: AsyncMock,
    install_addon_side_effect: Any | None,
) -> AsyncMock:
    """Mock install add-on."""
    supervisor_client.store.install_addon.side_effect = install_addon_side_effect
    return supervisor_client.store.install_addon


@pytest.fixture(name="start_addon_side_effect")
def start_addon_side_effect_fixture(
    addon_store_info: AsyncMock, addon_info: AsyncMock
) -> Any | None:
    """Return the start add-on options side effect."""
    from .hassio.common import mock_start_addon_side_effect  # noqa: PLC0415

    return mock_start_addon_side_effect(addon_store_info, addon_info)


@pytest.fixture(name="start_addon")
def start_addon_fixture(
    supervisor_client: AsyncMock, start_addon_side_effect: Any | None
) -> AsyncMock:
    """Mock start add-on."""
    supervisor_client.addons.start_addon.side_effect = start_addon_side_effect
    return supervisor_client.addons.start_addon


@pytest.fixture(name="restart_addon_side_effect")
def restart_addon_side_effect_fixture() -> Any | None:
    """Return the restart add-on options side effect."""
    return None


@pytest.fixture(name="restart_addon")
def restart_addon_fixture(
    supervisor_client: AsyncMock,
    restart_addon_side_effect: Any | None,
) -> AsyncMock:
    """Mock restart add-on."""
    supervisor_client.addons.restart_addon.side_effect = restart_addon_side_effect
    return supervisor_client.addons.restart_addon


@pytest.fixture(name="stop_addon")
def stop_addon_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock stop add-on."""
    return supervisor_client.addons.stop_addon


@pytest.fixture(name="addon_options")
def addon_options_fixture(addon_info: AsyncMock) -> dict[str, Any]:
    """Mock add-on options."""
    return addon_info.return_value.options


@pytest.fixture(name="set_addon_options_side_effect")
def set_addon_options_side_effect_fixture(
    addon_options: dict[str, Any],
) -> Any | None:
    """Return the set add-on options side effect."""
    from .hassio.common import mock_set_addon_options_side_effect  # noqa: PLC0415

    return mock_set_addon_options_side_effect(addon_options)


@pytest.fixture(name="set_addon_options")
def set_addon_options_fixture(
    supervisor_client: AsyncMock,
    set_addon_options_side_effect: Any | None,
) -> AsyncMock:
    """Mock set add-on options."""
    supervisor_client.addons.set_addon_options.side_effect = (
        set_addon_options_side_effect
    )
    return supervisor_client.addons.set_addon_options


@pytest.fixture(name="uninstall_addon")
def uninstall_addon_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock uninstall add-on."""
    return supervisor_client.addons.uninstall_addon


@pytest.fixture(name="create_backup")
def create_backup_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock create backup."""
    return supervisor_client.backups.partial_backup


@pytest.fixture(name="update_addon")
def update_addon_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock update add-on."""
    return supervisor_client.store.update_addon


@pytest.fixture(name="store_addons")
def store_addons_fixture() -> list[StoreAddon]:
    """Mock store addons list."""
    return []


@pytest.fixture(name="store_repositories")
def store_repositories_fixture() -> list[Repository]:
    """Mock store repositories list."""
    return []


@pytest.fixture(name="store_info")
def store_info_fixture(
    supervisor_client: AsyncMock,
    store_addons: list[StoreAddon],
    store_repositories: list[Repository],
) -> AsyncMock:
    """Mock store info."""
    supervisor_client.store.info.return_value = StoreInfo(
        addons=store_addons, repositories=store_repositories
    )
    return supervisor_client.store.info


@pytest.fixture(name="addon_stats")
def addon_stats_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock addon stats info."""
    from .hassio.common import mock_addon_stats  # noqa: PLC0415

    return mock_addon_stats(supervisor_client)


@pytest.fixture(name="addon_changelog")
def addon_changelog_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock addon changelog."""
    supervisor_client.store.addon_changelog.return_value = ""
    return supervisor_client.store.addon_changelog


@pytest.fixture(name="supervisor_is_connected")
def supervisor_is_connected_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock supervisor is connected."""
    supervisor_client.supervisor.ping.return_value = None
    return supervisor_client.supervisor.ping


@pytest.fixture(name="resolution_info")
def resolution_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock resolution info from supervisor."""
    supervisor_client.resolution.info.return_value = ResolutionInfo(
        suggestions=[],
        unsupported=[],
        unhealthy=[],
        issues=[],
        checks=[],
    )
    return supervisor_client.resolution.info


@pytest.fixture(name="resolution_suggestions_for_issue")
def resolution_suggestions_for_issue_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock suggestions by issue from supervisor resolution."""
    supervisor_client.resolution.suggestions_for_issue.return_value = []
    return supervisor_client.resolution.suggestions_for_issue


@pytest.fixture(name="jobs_info")
def jobs_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock jobs info from supervisor."""
    supervisor_client.jobs.info.return_value = JobsInfo(ignore_conditions=[], jobs=[])
    return supervisor_client.jobs.info


@pytest.fixture(name="os_yellow_info")
def os_yellow_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock yellow info API from supervisor OS."""
    supervisor_client.os.yellow_info.return_value = YellowInfo(
        disk_led=True, heartbeat_led=True, power_led=True
    )
    return supervisor_client.os.yellow_info


@pytest.fixture(name="os_green_info")
def os_green_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock green info API from supervisor OS."""
    supervisor_client.os.green_info.return_value = GreenInfo(
        activity_led=True, power_led=True, system_health_led=True
    )
    return supervisor_client.os.green_info


@pytest.fixture(name="supervisor_root_info")
def supervisor_root_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock root info API from supervisor."""
    supervisor_client.info.return_value = RootInfo(
        supervisor="222",
        homeassistant="0.110.0",
        hassos="1.2.3",
        docker="",
        hostname=None,
        operating_system=None,
        features=[],
        machine=None,
        machine_id=None,
        arch="",
        state=SupervisorState.RUNNING,
        supported_arch=[],
        supported=True,
        channel=UpdateChannel.STABLE,
        logging=LogLevel.INFO,
        timezone="Etc/UTC",
    )
    return supervisor_client.info


@pytest.fixture(name="host_info")
def host_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock host info API from supervisor."""
    supervisor_client.host.info.return_value = HostInfo(
        agent_version=None,
        apparmor_version=None,
        chassis="vm",
        virtualization=None,
        cpe=None,
        deployment=None,
        disk_free=1.6,
        disk_total=100.0,
        disk_used=98.4,
        disk_life_time=None,
        features=[],
        hostname=None,
        llmnr_hostname=None,
        kernel="4.19.0-6-amd64",
        operating_system="Debian GNU/Linux 10 (buster)",
        timezone=None,
        dt_utc=None,
        dt_synchronized=None,
        use_ntp=None,
        startup_time=None,
        boot_timestamp=None,
        broadcast_llmnr=None,
        broadcast_mdns=None,
    )
    return supervisor_client.host.info


@pytest.fixture(name="homeassistant_info")
def homeassistant_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock Home Assistant info API from supervisor."""
    supervisor_client.homeassistant.info.return_value = HomeAssistantInfo(
        version="1.0.0",
        version_latest="1.0.0",
        update_available=False,
        machine=None,
        ip_address=IPv4Address("172.30.32.1"),
        arch=None,
        image="homeassistant",
        boot=True,
        port=8123,
        ssl=False,
        watchdog=True,
        audio_input=None,
        audio_output=None,
        backups_exclude_database=False,
        duplicate_log_file=False,
    )
    return supervisor_client.homeassistant.info


@pytest.fixture(name="supervisor_info")
def supervisor_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock supervisor info API from supervisor."""
    supervisor_client.supervisor.info.return_value = SupervisorInfo(
        version="1.0.0",
        version_latest="1.0.0",
        update_available=False,
        channel=UpdateChannel.STABLE,
        arch="",
        supported=True,
        healthy=True,
        ip_address=IPv4Address("172.30.32.2"),
        timezone=None,
        logging=LogLevel.INFO,
        debug=False,
        debug_block=False,
        diagnostics=None,
        auto_update=True,
        country=None,
        detect_blocking_io=False,
        feature_flags={},
    )
    return supervisor_client.supervisor.info


@pytest.fixture(name="addons_list")
def addons_list_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock addons list API from supervisor."""
    supervisor_client.addons.list.return_value = [
        InstalledAddon(
            detached=False,
            advanced=False,
            available=True,
            build=False,
            description="",
            homeassistant=None,
            icon=False,
            logo=False,
            name="test",
            repository="core",
            slug="test",
            stage=AddonStage.STABLE,
            update_available=True,
            url="https://github.com/home-assistant/addons/test",
            version_latest="2.0.1",
            version="2.0.0",
            state=AddonState.STARTED,
        ),
        InstalledAddon(
            detached=False,
            advanced=False,
            available=True,
            build=False,
            description="",
            homeassistant=None,
            icon=False,
            logo=False,
            name="test2",
            repository="core",
            slug="test2",
            stage=AddonStage.STABLE,
            update_available=False,
            url="https://github.com",
            version_latest="3.1.0",
            version="3.1.0",
            state=AddonState.STOPPED,
        ),
    ]
    return supervisor_client.addons.list


@pytest.fixture(name="network_info")
def network_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock network info API from supervisor."""
    supervisor_client.network.info.return_value = NetworkInfo(
        interfaces=[],
        docker=DockerNetwork(
            interface="hassio",
            address=IPv4Network("172.30.32.0/23"),
            gateway=IPv4Address("172.30.32.1"),
            dns=IPv4Address("172.30.32.3"),
        ),
        host_internet=True,
        supervisor_internet=True,
    )
    return supervisor_client.network.info


@pytest.fixture(name="os_info")
def os_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock os info API from supervisor."""
    supervisor_client.os.info.return_value = OSInfo(
        version="1.0.0",
        version_latest="1.0.0",
        version_pending=None,
        update_available=False,
        board=None,
        boot=None,
        data_disk=None,
        boot_slots={},
    )
    return supervisor_client.os.info


@pytest.fixture(name="homeassistant_stats")
def homeassistant_stats_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock Home Assistant stats API from supervisor."""
    supervisor_client.homeassistant.stats.return_value = HomeAssistantStats(
        cpu_percent=0.99,
        memory_usage=182611968,
        memory_limit=3977146368,
        memory_percent=4.59,
        network_rx=362570232,
        network_tx=82374138,
        blk_read=46010945536,
        blk_write=15051526144,
    )
    return supervisor_client.homeassistant.stats


@pytest.fixture(name="supervisor_stats")
def supervisor_stats_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock supervisor stats API from supervisor."""
    supervisor_client.supervisor.stats.return_value = SupervisorStats(
        cpu_percent=0.99,
        memory_usage=182611968,
        memory_limit=3977146368,
        memory_percent=4.59,
        network_rx=362570232,
        network_tx=82374138,
        blk_read=46010945536,
        blk_write=15051526144,
    )
    return supervisor_client.supervisor.stats


@pytest.fixture(name="ingress_panels")
def ingress_panels_fixture(supervisor_client: AsyncMock) -> AsyncMock:
    """Mock ingress panels API from supervisor."""
    supervisor_client.ingress.panels.return_value = {}
    return supervisor_client.ingress.panels


@pytest.fixture(name="supervisor_client")
def supervisor_client() -> Generator[AsyncMock]:
    """Mock the supervisor client."""
    supervisor_client = AsyncMock(spec=SupervisorClient)
    supervisor_client.addons = AsyncMock(spec=AddonsClient)
    supervisor_client.backups = AsyncMock(spec=BackupsClient)
    supervisor_client.discovery = AsyncMock(spec=DiscoveryClient)
    supervisor_client.homeassistant = AsyncMock(spec=HomeAssistantClient)
    supervisor_client.host = AsyncMock(spec=HostClient)
    supervisor_client.ingress = AsyncMock(spec=IngressClient)
    supervisor_client.jobs = AsyncMock(spec=JobsClient)
    supervisor_client.jobs.info.return_value = JobsInfo(ignore_conditions=[], jobs=[])
    supervisor_client.mounts = AsyncMock(spec=MountsClient)
    supervisor_client.mounts.info.return_value = MagicMock(
        spec=MountsInfo, default_backup_mount=None, mounts=[]
    )
    supervisor_client.network = AsyncMock(spec=NetworkClient)
    supervisor_client.os = AsyncMock(spec=OSClient)
    supervisor_client.resolution = AsyncMock(spec=ResolutionClient)
    supervisor_client.supervisor = AsyncMock(spec=SupervisorManagementClient)
    supervisor_client.store = AsyncMock(spec=StoreClient)

    with (
        patch(
            "homeassistant.components.hassio.get_supervisor_client",
            return_value=supervisor_client,
        ),
        patch(
            "homeassistant.components.hassio.handler.get_supervisor_client",
            return_value=supervisor_client,
        ),
        patch(
            "homeassistant.components.hassio.addon_manager.get_supervisor_client",
            return_value=supervisor_client,
        ),
        patch(
            "homeassistant.components.hassio.addon_panel.get_supervisor_client",
            return_value=supervisor_client,
        ),
        patch(
            "homeassistant.components.hassio.backup.get_supervisor_client",
            return_value=supervisor_client,
        ),
        patch(
            "homeassistant.components.hassio.discovery.get_supervisor_client",
            return_value=supervisor_client,
        ),
        patch(
            "homeassistant.components.hassio.coordinator.get_supervisor_client",
            return_value=supervisor_client,
        ),
        patch(
            "homeassistant.components.hassio.repairs.get_supervisor_client",
            return_value=supervisor_client,
        ),
        patch(
            "homeassistant.components.hassio.services.get_supervisor_client",
            return_value=supervisor_client,
        ),
        patch(
            "homeassistant.components.hassio.update_helper.get_supervisor_client",
            return_value=supervisor_client,
        ),
        patch(
            "homeassistant.components.homeassistant_hardware.util."
            "get_supervisor_client",
            return_value=supervisor_client,
        ),
    ):
        yield supervisor_client


def _validate_translation_placeholders(
    full_key: str,
    translation: str,
    description_placeholders: dict[str, str] | None,
    translation_errors: dict[str, str],
) -> str | None:
    """Raise if translation exists with missing placeholders."""
    tuples = list(string.Formatter().parse(translation))
    for _, placeholder, _, _ in tuples:
        if placeholder is None:
            continue
        if (
            description_placeholders is None
            or placeholder not in description_placeholders
        ):
            translation_errors[full_key] = (
                f"Description not found for placeholder `{placeholder}` in {full_key}"
            )


async def _validate_translation(
    hass: HomeAssistant,
    translation_errors: dict[str, str],
    ignore_translations_for_mock_domains: set[str],
    category: str,
    component: str,
    key: str,
    description_placeholders: Mapping[str, str] | None,
    *,
    translation_required: bool = True,
) -> None:
    """Raise if translation doesn't exist."""
    full_key = f"component.{component}.{category}.{key}"
    if component in ignore_translations_for_mock_domains:
        try:
            integration = await loader.async_get_integration(hass, component)
        except loader.IntegrationNotFound:
            return
        component_paths = components.__path__
        if not any(
            Path(f"{component_path}/{component}") == integration.file_path
            for component_path in component_paths
        ):
            return
        # If the integration exists, translation errors should be ignored via the
        # ignore_missing_translations fixture instead of the
        # ignore_translations_for_mock_domains fixture.
        translation_errors[full_key] = f"The integration '{component}' exists"
        return

    translations = await async_get_translations(hass, "en", category, [component])

    if full_key.endswith("."):
        for subkey, translation in translations.items():
            if subkey.startswith(full_key):
                _validate_translation_placeholders(
                    subkey, translation, description_placeholders, translation_errors
                )
        return
    if (translation := translations.get(full_key)) is not None:
        _validate_translation_placeholders(
            full_key, translation, description_placeholders, translation_errors
        )
        return

    if not translation_required:
        return

    if full_key not in translation_errors:
        for k in translation_errors:
            if k.endswith(".") and full_key.startswith(k):
                full_key = k
                break
    if translation_errors.get(full_key) in {"used", "unused"}:
        # If the integration does not exist, translation errors should be ignored
        # via the ignore_translations_for_mock_domains fixture instead of the
        # ignore_missing_translations fixture.
        try:
            await loader.async_get_integration(hass, component)
        except loader.IntegrationNotFound:
            translation_errors[full_key] = (
                f"Translation not found for {component}: `{category}.{key}`. "
                f"The integration '{component}' does not exist."
            )
            return

        # This translation key is in the ignore list, mark it as used
        translation_errors[full_key] = "used"
        return

    translation_errors[full_key] = (
        f"Translation not found for {component}: `{category}.{key}`. "
        f"Please add to homeassistant/components/{component}/strings.json"
    )


@pytest.fixture
def ignore_missing_translations() -> str | list[str]:
    """Ignore specific missing translations.

    Override or parametrize this fixture with a fixture that returns
    a list of missing translation that should be ignored.
    """
    return []


@pytest.fixture
def ignore_translations_for_mock_domains() -> str | list[str]:
    """Don't validate translations for specific domains.

    Override or parametrize this fixture with a fixture that returns
    a list of domains for which translations should not be validated.
    This should only be used when testing mocked integrations.
    """
    return []


@lru_cache
def _get_integration_quality_scale(integration: str) -> dict[str, Any]:
    """Get the quality scale for an integration."""
    try:
        return yaml_util.load_yaml_dict(
            f"homeassistant/components/{integration}/quality_scale.yaml"
        ).get("rules", {})
    except FileNotFoundError:
        return {}


def _get_integration_quality_scale_rule(integration: str, rule: str) -> str:
    """Get the quality scale for an integration."""
    quality_scale = _get_integration_quality_scale(integration)
    if not quality_scale or rule not in quality_scale:
        return "todo"
    status = quality_scale[rule]
    return status if isinstance(status, str) else status["status"]


async def _check_step_or_section_translations(
    hass: HomeAssistant,
    translation_errors: dict[str, str],
    category: str,
    integration: str,
    translation_prefix: str,
    description_placeholders: dict[str, str],
    data_schema: vol.Schema | None,
    ignore_translations_for_mock_domains: set[str],
) -> None:
    # neither title nor description are required
    # - title defaults to integration name
    # - description is optional
    for header in ("title", "description"):
        await _validate_translation(
            hass,
            translation_errors,
            ignore_translations_for_mock_domains,
            category,
            integration,
            f"{translation_prefix}.{header}",
            description_placeholders,
            translation_required=False,
        )

    if not data_schema:
        return

    for data_key, data_value in data_schema.schema.items():
        if isinstance(data_value, section):
            # check the nested section
            await _check_step_or_section_translations(
                hass,
                translation_errors,
                category,
                integration,
                f"{translation_prefix}.sections.{data_key}",
                description_placeholders,
                data_value.schema,
                ignore_translations_for_mock_domains,
            )
            continue
        iqs_config_flow = _get_integration_quality_scale_rule(
            integration, "config-flow"
        )
        # data and data_description are compulsory
        for header in ("data", "data_description"):
            await _validate_translation(
                hass,
                translation_errors,
                ignore_translations_for_mock_domains,
                category,
                integration,
                f"{translation_prefix}.{header}.{data_key}",
                description_placeholders,
                translation_required=(iqs_config_flow == "done"),
            )


async def _check_config_flow_result_translations(
    manager: FlowManager,
    flow: FlowHandler,
    result: FlowResult[FlowContext, str],
    translation_errors: dict[str, str],
    ignore_translations_for_mock_domains: set[str],
) -> None:
    if result["type"] is FlowResultType.CREATE_ENTRY:
        # No need to check translations for a completed flow
        return

    key_prefix = ""
    description_placeholders = result.get("description_placeholders")
    if isinstance(manager, ConfigEntriesFlowManager):
        category = "config"
        integration = flow.handler
    elif isinstance(manager, OptionsFlowManager):
        category = "options"
        integration = flow.hass.config_entries.async_get_entry(flow.handler).domain
    elif isinstance(manager, repairs.RepairsFlowManager):
        category = "issues"
        integration = flow.handler
        issue_id = flow.issue_id
        issue = ir.async_get(flow.hass).async_get_issue(integration, issue_id)
        if issue is None:
            # Issue was deleted mid-flow (e.g., config entry removed), skip check
            return
        key_prefix = f"{issue.translation_key}.fix_flow."
        description_placeholders = {
            # Both are used in issue translations, and description_placeholders
            # takes precedence over translation_placeholders
            **(issue.translation_placeholders or {}),
            **(description_placeholders or {}),
        }
    else:
        return

    # Check if this flow has been seen before
    # Gets set to False on first run, and to True on subsequent runs
    setattr(flow, "__flow_seen_before", hasattr(flow, "__flow_seen_before"))

    if result["type"] is FlowResultType.FORM:
        if step_id := result.get("step_id"):
            await _check_step_or_section_translations(
                flow.hass,
                translation_errors,
                category,
                integration,
                f"{key_prefix}step.{step_id}",
                description_placeholders,
                result["data_schema"],
                ignore_translations_for_mock_domains,
            )

        if errors := result.get("errors"):
            for error in errors.values():
                await _validate_translation(
                    flow.hass,
                    translation_errors,
                    ignore_translations_for_mock_domains,
                    category,
                    integration,
                    f"{key_prefix}error.{error}",
                    description_placeholders,
                )
        return

    if result["type"] is FlowResultType.ABORT:
        # We don't need translations for a discovery flow which immediately
        # aborts, since such flows won't be seen by users
        if not flow.__flow_seen_before and flow.source in DISCOVERY_SOURCES:
            return
        await _validate_translation(
            flow.hass,
            translation_errors,
            ignore_translations_for_mock_domains,
            category,
            integration,
            f"{key_prefix}abort.{result['reason']}",
            description_placeholders,
        )


async def _check_create_issue_translations(
    issue_registry: ir.IssueRegistry,
    issue: ir.IssueEntry,
    translation_errors: dict[str, str],
    ignore_translations_for_mock_domains: set[str],
) -> None:
    if issue.translation_key is None:
        # `translation_key` is only None on dismissed issues
        return
    await _validate_translation(
        issue_registry.hass,
        translation_errors,
        ignore_translations_for_mock_domains,
        "issues",
        issue.domain,
        f"{issue.translation_key}.title",
        issue.translation_placeholders,
    )
    if (
        not issue.is_fixable
        and issue.translation_key
        not in ir.FRONTEND_HANDLED_ISSUES.get(issue.domain, ())
    ):
        # Description is required for non-fixable issues
        await _validate_translation(
            issue_registry.hass,
            translation_errors,
            ignore_translations_for_mock_domains,
            "issues",
            issue.domain,
            f"{issue.translation_key}.description",
            issue.translation_placeholders,
        )


def _get_request_quality_scale(
    request: pytest.FixtureRequest, rule: str
) -> QualityScaleStatus:
    if not (match := RE_REQUEST_DOMAIN.match(str(request.path))):
        return QualityScaleStatus.TODO
    integration = match.groups(1)[0]
    return get_quality_scale(integration).get(rule, QualityScaleStatus.TODO)


async def _check_exception_translation(
    hass: HomeAssistant,
    exception: HomeAssistantError,
    translation_errors: dict[str, str],
    request: pytest.FixtureRequest,
    ignore_translations_for_mock_domains: set[str],
) -> None:
    if exception.translation_key is None:
        if (
            _get_request_quality_scale(request, "exception-translations")
            is QualityScaleStatus.DONE
        ):
            translation_errors["quality_scale"] = (
                f"Found untranslated {type(exception).__name__} exception: {exception}"
            )
        return
    await _validate_translation(
        hass,
        translation_errors,
        ignore_translations_for_mock_domains,
        "exceptions",
        exception.translation_domain,
        f"{exception.translation_key}.message",
        exception.translation_placeholders,
    )


_DYNAMIC_SERVICE_DOMAINS = {
    "esphome",
    "notify",
    "rest_command",
    "script",
    "shell_command",
    "tts",
}
"""These domains create services dynamically.

name/description translations are not required.
"""


async def _check_service_registration_translation(
    hass: HomeAssistant,
    domain: str,
    service_name: str,
    description_placeholders: Mapping[str, str] | None,
    translation_errors: dict[str, str],
    ignore_translations_for_mock_domains: set[str],
) -> None:
    # Use trailing . to check all subkeys
    # This validates placeholders only, and only if the translation exists
    await _validate_translation(
        hass,
        translation_errors,
        ignore_translations_for_mock_domains,
        "services",
        domain,
        f"{service_name}.",
        description_placeholders,
    )
    # Service `name` and `description` should be compulsory
    # unless for specific domains where the services are dynamically created
    if domain not in _DYNAMIC_SERVICE_DOMAINS:
        for subkey in ("name", "description"):
            await _validate_translation(
                hass,
                translation_errors,
                ignore_translations_for_mock_domains,
                "services",
                domain,
                f"{service_name}.{subkey}",
                description_placeholders,
                translation_required=True,
            )


@pytest.fixture(autouse=True)
async def check_translations(
    ignore_missing_translations: str | list[str],
    ignore_translations_for_mock_domains: str | list[str],
    request: pytest.FixtureRequest,
) -> AsyncGenerator[None]:
    """Check that translation requirements are met.

    Current checks:
    - data entry flow results (ConfigFlow/OptionsFlow/RepairFlow)
    - issue registry entries
    - action (service) exceptions
    """
    if not isinstance(ignore_missing_translations, list):
        ignore_missing_translations = [ignore_missing_translations]

    if not isinstance(ignore_translations_for_mock_domains, list):
        ignored_domains = {ignore_translations_for_mock_domains}
    else:
        ignored_domains = set(ignore_translations_for_mock_domains)

    # Set all ignored translation keys to "unused"
    translation_errors = dict.fromkeys(ignore_missing_translations, "unused")

    translation_coros = set()

    # Keep reference to original functions
    _original_flow_manager_async_handle_step = FlowManager._async_handle_step
    _original_issue_registry_async_create_issue = ir.IssueRegistry.async_get_or_create
    _original_service_registry_async_call = ServiceRegistry.async_call
    _original_service_registry_async_register = ServiceRegistry.async_register

    # Prepare override functions
    async def _flow_manager_async_handle_step(
        self: FlowManager, flow: FlowHandler, *args
    ) -> FlowResult:
        result = await _original_flow_manager_async_handle_step(self, flow, *args)
        await _check_config_flow_result_translations(
            self, flow, result, translation_errors, ignored_domains
        )
        return result

    def _issue_registry_async_create_issue(
        self: ir.IssueRegistry, domain: str, issue_id: str, *args, **kwargs
    ) -> ir.IssueEntry:
        result = _original_issue_registry_async_create_issue(
            self, domain, issue_id, *args, **kwargs
        )
        translation_coros.add(
            _check_create_issue_translations(
                self, result, translation_errors, ignored_domains
            )
        )
        return result

    async def _service_registry_async_call(
        self: ServiceRegistry,
        domain: str,
        service: str,
        service_data: dict[str, Any] | None = None,
        blocking: bool = False,
        context: Context | None = None,
        target: dict[str, Any] | None = None,
        return_response: bool = False,
    ) -> ServiceResponse:
        try:
            return await _original_service_registry_async_call(
                self,
                domain,
                service,
                service_data,
                blocking,
                context,
                target,
                return_response,
            )
        except HomeAssistantError as err:
            translation_coros.add(
                _check_exception_translation(
                    self._hass,
                    err,
                    translation_errors,
                    request,
                    ignored_domains,
                )
            )
            raise

    @callback
    def _service_registry_async_register(
        self: ServiceRegistry,
        domain: str,
        service: str,
        service_func: Callable[
            [ServiceCall],
            Coroutine[Any, Any, ServiceResponse | EntityServiceResponse]
            | ServiceResponse
            | EntityServiceResponse
            | None,
        ],
        schema: VolSchemaType | None = None,
        supports_response: SupportsResponse = SupportsResponse.NONE,
        job_type: HassJobType | None = None,
        *,
        description_placeholders: Mapping[str, str] | None = None,
    ) -> None:
        if (
            (current_frame := inspect.currentframe()) is None
            or (caller := current_frame.f_back) is None
            or (
                # async_mock_service is used in tests to register test services
                caller.f_code.co_name != "async_mock_service"
                # ServiceRegistry.async_register can also be called directly in
                # a test module
                and not caller.f_code.co_filename.startswith(
                    str(Path(__file__).parents[0])
                )
            )
        ):
            translation_coros.add(
                _check_service_registration_translation(
                    self._hass,
                    domain,
                    service,
                    description_placeholders,
                    translation_errors,
                    ignored_domains,
                )
            )

        _original_service_registry_async_register(
            self,
            domain,
            service,
            service_func,
            schema,
            supports_response,
            job_type,
            description_placeholders=description_placeholders,
        )

    # Use override functions
    with (
        patch(
            "homeassistant.data_entry_flow.FlowManager._async_handle_step",
            _flow_manager_async_handle_step,
        ),
        patch(
            "homeassistant.helpers.issue_registry.IssueRegistry.async_get_or_create",
            _issue_registry_async_create_issue,
        ),
        patch(
            "homeassistant.core.ServiceRegistry.async_call",
            _service_registry_async_call,
        ),
        patch(
            "homeassistant.core.ServiceRegistry.async_register",
            _service_registry_async_register,
        ),
    ):
        yield

    await asyncio.gather(*translation_coros)

    # Run final checks
    unused_ignore = [k for k, v in translation_errors.items() if v == "unused"]
    if unused_ignore:
        # Some ignored translations were not used
        pytest.fail(
            f"Unused ignore translations: {', '.join(unused_ignore)}. "
            "Please remove them from the ignore_missing_translations fixture."
        )
    for description in translation_errors.values():
        if description != "used":
            pytest.fail(description)
