"""The tests for the hassio component."""

from dataclasses import replace
from datetime import timedelta
import os
from pathlib import PurePath
from typing import Any
from unittest.mock import ANY, AsyncMock, Mock, call, patch
from uuid import uuid4

from aiohasupervisor import SupervisorError
from aiohasupervisor.models import (
    AddonsStats,
    AddonStage,
    AddonState,
    CIFSMountResponse,
    Folder,
    FullBackupOptions,
    HomeAssistantOptions,
    InstalledAddon,
    InstalledAddonComplete,
    MountsInfo,
    MountState,
    MountType,
    MountUsage,
    NewBackup,
    PartialBackupOptions,
    PartialRestoreOptions,
    SupervisorOptions,
)
from freezegun.api import FrozenDateTimeFactory
import pytest
from voluptuous import Invalid

from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components import frontend, hassio
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.hassio import (
    ADDONS_COORDINATOR,
    DOMAIN,
    get_addons_info,
    get_addons_list,
    get_addons_stats,
    get_core_info,
    get_core_stats,
    get_host_info,
    get_info,
    get_network_info,
    get_os_info,
    get_store,
    get_supervisor_info,
    get_supervisor_stats,
    hostname_from_addon_slug,
)
from homeassistant.components.hassio.config import STORAGE_KEY
from homeassistant.components.hassio.const import (
    DATA_KEY_SUPERVISOR_ISSUES,
    HASSIO_MAIN_UPDATE_INTERVAL,
    REQUEST_REFRESH_DELAY,
)
from homeassistant.components.homeassistant import (
    DOMAIN as HOMEASSISTANT_DOMAIN,
    SERVICE_UPDATE_ENTITY,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.hassio import is_hassio
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from homeassistant.util.yaml import load_yaml_dict

from tests.common import MockConfigEntry, async_fire_time_changed

MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}


@pytest.fixture(autouse=True)
def mock_all(
    store_info: AsyncMock,
    addon_info: AsyncMock,
    addon_stats: AsyncMock,
    addon_changelog: AsyncMock,
    resolution_info: AsyncMock,
    jobs_info: AsyncMock,
    host_info: AsyncMock,
    supervisor_root_info: AsyncMock,
    homeassistant_info: AsyncMock,
    supervisor_info: AsyncMock,
    addons_list: AsyncMock,
    network_info: AsyncMock,
    os_info: AsyncMock,
    homeassistant_stats: AsyncMock,
    supervisor_stats: AsyncMock,
    addon_installed: AsyncMock,
    ingress_panels: AsyncMock,
) -> None:
    """Mock all setup requests."""
    addons_list.return_value[0] = replace(
        addons_list.return_value[0],
        version="1.0.0",
        version_latest="1.0.0",
        update_available=False,
        state=AddonState.STOPPED,
    )
    addons_list.return_value[1] = replace(
        addons_list.return_value[1],
        version="1.0.0",
        version_latest="1.0.0",
    )
    addon_installed.return_value.state = AddonState.STOPPED

    async def mock_addon_stats(addon: str) -> AddonsStats:
        """Mock addon stats for test and test2."""
        if addon in {"test2", "test3"}:
            return AddonsStats(
                cpu_percent=0.8,
                memory_usage=51941376,
                memory_limit=3977146368,
                memory_percent=1.31,
                network_rx=31338284,
                network_tx=15692900,
                blk_read=740077568,
                blk_write=6004736,
            )
        return AddonsStats(
            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,
        )

    addon_stats.side_effect = mock_addon_stats

    def mock_addon_info(slug: str):
        addon = Mock(
            spec=InstalledAddonComplete,
            to_dict=addon_installed.return_value.to_dict,
            **addon_installed.return_value.to_dict(),
        )
        if slug == "test":
            addon.name = "test"
            addon.slug = "test"
            addon.url = "https://github.com/home-assistant/addons/test"
            addon.auto_update = True
        else:
            addon.name = "test2"
            addon.slug = "test2"
            addon.url = "https://github.com"
            addon.auto_update = False

        return addon

    addon_info.side_effect = mock_addon_info


async def test_setup_api_ping(
    hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
    """Test setup with API ping."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {})
        await hass.async_block_till_done()

    assert result
    assert len(supervisor_client.mock_calls) == 16
    assert get_core_info(hass)["version_latest"] == "1.0.0"
    assert is_hassio(hass)


async def test_setup_api_ping_fails(
    hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
    """Test that a failed ping raises ConfigEntryNotReady and retries."""
    supervisor_client.supervisor.ping.side_effect = SupervisorError

    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {})
        await hass.async_block_till_done()

    # async_setup succeeds (domain registered), but the config entry is in retry
    assert result
    assert is_hassio(hass)
    entry = hass.config_entries.async_entries("hassio")[0]
    assert entry.state is ConfigEntryState.SETUP_RETRY


async def test_setup_app_panel(hass: HomeAssistant) -> None:
    """Test app panel is registered."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {})
        await hass.async_block_till_done()
        assert result

    panels = hass.data[frontend.DATA_PANELS]

    assert panels.get("app").to_response() == {
        "component_name": "app",
        "icon": None,
        "title": None,
        "default_visible": True,
        "config": None,
        "url_path": "app",
        "require_admin": False,
        "show_in_sidebar": True,
        "config_panel_domain": None,
    }


async def test_setup_api_push_api_data(
    hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
    """Test setup with API push."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(
            hass, "hassio", {"http": {"server_port": 9999}, "hassio": {}}
        )
        await hass.async_block_till_done()

    assert result
    assert len(supervisor_client.mock_calls) == 16
    supervisor_client.homeassistant.set_options.assert_called_once_with(
        HomeAssistantOptions(ssl=False, port=9999, refresh_token=ANY)
    )


async def test_setup_api_push_api_data_error(
    hass: HomeAssistant, supervisor_client: AsyncMock, caplog: pytest.LogCaptureFixture
) -> None:
    """Test setup with error while pushing core config data to API."""
    supervisor_client.homeassistant.set_options.side_effect = SupervisorError("boom")
    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
        await hass.async_block_till_done()

    assert result
    assert len(supervisor_client.mock_calls) == 16
    assert "Failed to update Home Assistant options in Supervisor: boom" in caplog.text


async def test_setup_api_push_api_data_server_host(
    hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
    """Test setup with API push with active server host."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(
            hass,
            "hassio",
            {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
        )
        await hass.async_block_till_done()

    assert result
    assert len(supervisor_client.mock_calls) == 16
    supervisor_client.homeassistant.set_options.assert_called_once_with(
        HomeAssistantOptions(ssl=False, port=9999, refresh_token=ANY, watchdog=False)
    )


async def test_setup_api_push_api_data_default(
    hass: HomeAssistant, hass_storage: dict[str, Any], supervisor_client: AsyncMock
) -> None:
    """Test setup with API push default data."""
    with (
        patch.dict(os.environ, MOCK_ENVIRON),
        patch("homeassistant.components.hassio.config.STORE_DELAY_SAVE", 0),
    ):
        result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
        await hass.async_block_till_done()

    assert result
    assert len(supervisor_client.mock_calls) == 16
    supervisor_client.homeassistant.set_options.assert_called_once_with(
        HomeAssistantOptions(ssl=False, port=8123, refresh_token=ANY)
    )
    refresh_token = (
        supervisor_client.homeassistant.set_options.mock_calls[0].args[0].refresh_token
    )
    hassio_user = await hass.auth.async_get_user(
        hass_storage[STORAGE_KEY]["data"]["hassio_user"]
    )
    assert hassio_user is not None
    assert hassio_user.system_generated
    assert len(hassio_user.groups) == 1
    assert hassio_user.groups[0].id == GROUP_ID_ADMIN
    assert hassio_user.name == "Supervisor"
    for token in hassio_user.refresh_tokens.values():
        if token.token == refresh_token:
            break
    else:
        pytest.fail("refresh token not found")


async def test_setup_adds_admin_group_to_user(
    hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
    """Test setup with API push default data."""
    # Create user without admin
    user = await hass.auth.async_create_system_user("Hass.io")
    assert not user.is_admin
    await hass.auth.async_create_refresh_token(user)

    hass_storage[STORAGE_KEY] = {
        "data": {"hassio_user": user.id},
        "key": STORAGE_KEY,
        "version": 1,
    }

    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
        assert result

    assert user.is_admin


async def test_setup_migrate_user_name(
    hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
    """Test setup with migrating the user name."""
    # Create user with old name
    user = await hass.auth.async_create_system_user("Hass.io")
    await hass.auth.async_create_refresh_token(user)

    hass_storage[STORAGE_KEY] = {
        "data": {"hassio_user": user.id},
        "key": STORAGE_KEY,
        "version": 1,
    }

    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
        assert result

    assert user.name == "Supervisor"


async def test_setup_api_existing_hassio_user(
    hass: HomeAssistant, hass_storage: dict[str, Any], supervisor_client: AsyncMock
) -> None:
    """Test setup with API push default data."""
    user = await hass.auth.async_create_system_user("Hass.io test")
    token = await hass.auth.async_create_refresh_token(user)
    hass_storage[STORAGE_KEY] = {"version": 1, "data": {"hassio_user": user.id}}
    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
        await hass.async_block_till_done()

    assert result
    assert len(supervisor_client.mock_calls) == 16
    supervisor_client.homeassistant.set_options.assert_called_once_with(
        HomeAssistantOptions(ssl=False, port=8123, refresh_token=token.token)
    )


async def test_setup_core_push_config(
    hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
    """Test setup with API push default data."""
    hass.config.time_zone = "testzone"

    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {"hassio": {}})
        await hass.async_block_till_done()

    assert result
    assert len(supervisor_client.mock_calls) == 16
    supervisor_client.supervisor.set_options.assert_called_once_with(
        SupervisorOptions(timezone="testzone")
    )

    with patch("homeassistant.util.dt.set_default_time_zone"):
        await hass.config.async_update(time_zone="America/New_York", country="US")
    await hass.async_block_till_done()
    supervisor_client.supervisor.set_options.assert_called_with(
        SupervisorOptions(timezone="America/New_York", country="US")
    )


async def test_setup_core_push_config_error(
    hass: HomeAssistant, supervisor_client: AsyncMock, caplog: pytest.LogCaptureFixture
) -> None:
    """Test setup with error while pushing supervisor config data to API."""
    hass.config.time_zone = "testzone"
    supervisor_client.supervisor.set_options.side_effect = SupervisorError("boom")

    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {"hassio": {}})
        await hass.async_block_till_done()

    assert result
    assert len(supervisor_client.mock_calls) == 16
    assert "Failed to update Supervisor options: boom" in caplog.text


async def test_setup_hassio_no_additional_data(
    hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
    """Test setup with API push default data."""
    with (
        patch.dict(os.environ, MOCK_ENVIRON),
        patch.dict(os.environ, {"SUPERVISOR_TOKEN": "123456"}),
    ):
        result = await async_setup_component(hass, "hassio", {"hassio": {}})
        await hass.async_block_till_done()

    assert result
    assert len(supervisor_client.mock_calls) == 16


async def test_fail_setup_without_environ_var(hass: HomeAssistant) -> None:
    """Fail setup if no environ variable set."""
    with patch.dict(os.environ, {}, clear=True):
        result = await async_setup_component(hass, "hassio", {})
        assert not result


async def test_warn_when_cannot_connect(
    hass: HomeAssistant,
    supervisor_is_connected: AsyncMock,
) -> None:
    """Test that a failed ping puts the config entry in retry state."""
    supervisor_is_connected.side_effect = SupervisorError
    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {})
        assert result

    assert is_hassio(hass)
    entry = hass.config_entries.async_entries("hassio")[0]
    assert entry.state is ConfigEntryState.SETUP_RETRY


@pytest.mark.usefixtures("hassio_env")
async def test_service_register(hass: HomeAssistant) -> None:
    """Check if service will be setup."""
    assert await async_setup_component(hass, "hassio", {})
    # New app services
    assert hass.services.has_service("hassio", "app_start")
    assert hass.services.has_service("hassio", "app_stop")
    assert hass.services.has_service("hassio", "app_restart")
    assert hass.services.has_service("hassio", "app_stdin")
    # Legacy addon services (deprecated)
    assert hass.services.has_service("hassio", "addon_start")
    assert hass.services.has_service("hassio", "addon_stop")
    assert hass.services.has_service("hassio", "addon_restart")
    assert hass.services.has_service("hassio", "addon_stdin")
    # Other services
    assert hass.services.has_service("hassio", "host_shutdown")
    assert hass.services.has_service("hassio", "host_reboot")
    assert hass.services.has_service("hassio", "host_reboot")
    assert hass.services.has_service("hassio", "backup_full")
    assert hass.services.has_service("hassio", "backup_partial")
    assert hass.services.has_service("hassio", "restore_full")
    assert hass.services.has_service("hassio", "restore_partial")
    assert hass.services.has_service("hassio", "mount_reload")


@pytest.mark.parametrize(
    "app_or_addon",
    ["app", "addon"],
)
@pytest.mark.freeze_time("2021-11-13 11:48:00")
async def test_service_calls(
    hass: HomeAssistant,
    supervisor_client: AsyncMock,
    supervisor_is_connected: AsyncMock,
    app_or_addon: str,
) -> None:
    """Call service and check the API calls behind that."""
    supervisor_is_connected.side_effect = SupervisorError
    with patch.dict(os.environ, MOCK_ENVIRON):
        assert await async_setup_component(hass, "hassio", {})
        await hass.async_block_till_done()

    supervisor_client.reset_mock()

    await hass.services.async_call(
        "hassio", f"{app_or_addon}_start", {app_or_addon: "test"}
    )
    await hass.services.async_call(
        "hassio", f"{app_or_addon}_stop", {app_or_addon: "test"}
    )
    await hass.services.async_call(
        "hassio", f"{app_or_addon}_restart", {app_or_addon: "test"}
    )
    await hass.services.async_call(
        "hassio", f"{app_or_addon}_stdin", {app_or_addon: "test", "input": "test"}
    )
    await hass.services.async_call(
        "hassio",
        f"{app_or_addon}_stdin",
        {app_or_addon: "test", "input": {"hello": "world"}},
    )
    await hass.async_block_till_done()

    supervisor_client.addons.start_addon.assert_called_once_with("test")
    supervisor_client.addons.stop_addon.assert_called_once_with("test")
    supervisor_client.addons.restart_addon.assert_called_once_with("test")
    assert (
        call("test", b'"test"') in supervisor_client.addons.write_addon_stdin.mock_calls
    )
    assert (
        call("test", b'{"hello": "world"}')
        in supervisor_client.addons.write_addon_stdin.mock_calls
    )

    await hass.services.async_call("hassio", "host_shutdown", {})
    await hass.services.async_call("hassio", "host_reboot", {})
    await hass.async_block_till_done()

    supervisor_client.host.shutdown.assert_called_once_with()
    supervisor_client.host.reboot.assert_called_once_with()

    supervisor_client.backups.full_backup.return_value = NewBackup(
        job_id=uuid4(), slug="full"
    )
    supervisor_client.backups.partial_backup.return_value = NewBackup(
        job_id=uuid4(), slug="partial"
    )

    full_backup = await hass.services.async_call(
        "hassio", "backup_full", {}, blocking=True, return_response=True
    )
    supervisor_client.backups.full_backup.assert_called_once_with(
        FullBackupOptions(name="2021-11-13 03:48:00")
    )
    assert full_backup == {"backup": "full"}

    partial_backup = await hass.services.async_call(
        "hassio",
        "backup_partial",
        {
            "homeassistant": True,
            f"{app_or_addon}s": ["test"],
            "folders": ["ssl"],
            "password": "123456",
        },
        blocking=True,
        return_response=True,
    )
    supervisor_client.backups.partial_backup.assert_called_once_with(
        PartialBackupOptions(
            name="2021-11-13 03:48:00",
            homeassistant=True,
            addons={"test"},
            folders={Folder.SSL},
            password="123456",
        )
    )
    assert partial_backup == {"backup": "partial"}

    await hass.services.async_call("hassio", "restore_full", {"slug": "test"})
    await hass.services.async_call(
        "hassio",
        "restore_partial",
        {
            "slug": "test",
            "homeassistant": False,
            f"{app_or_addon}s": ["test"],
            "folders": ["ssl"],
            "password": "123456",
        },
    )
    await hass.async_block_till_done()

    supervisor_client.backups.full_restore.assert_called_once_with("test", None)
    supervisor_client.backups.partial_restore.assert_called_once_with(
        "test",
        PartialRestoreOptions(
            homeassistant=False,
            addons={"test"},
            folders={Folder.SSL},
            password="123456",
        ),
    )

    await hass.services.async_call(
        "hassio",
        "backup_full",
        {
            "name": "backup_name",
            "location": "backup_share",
            "homeassistant_exclude_database": True,
        },
    )
    await hass.async_block_till_done()
    supervisor_client.backups.full_backup.assert_called_with(
        FullBackupOptions(
            name="backup_name",
            location="backup_share",
            homeassistant_exclude_database=True,
        )
    )

    await hass.services.async_call(
        "hassio",
        "backup_full",
        {
            "location": "/backup",
        },
    )
    await hass.async_block_till_done()
    supervisor_client.backups.full_backup.assert_called_with(
        FullBackupOptions(name="2021-11-13 03:48:00", location=None)
    )

    # check backup with different timezone
    await hass.config.async_update(time_zone="Europe/London")
    await hass.async_block_till_done()

    await hass.services.async_call(
        "hassio",
        "backup_full",
        {
            "location": "/backup",
        },
    )
    await hass.async_block_till_done()
    supervisor_client.backups.full_backup.assert_called_with(
        FullBackupOptions(name="2021-11-13 11:48:00", location=None)
    )


@pytest.mark.parametrize(
    "app_or_addon",
    ["app", "addon"],
)
async def test_invalid_service_calls(hass: HomeAssistant, app_or_addon: str) -> None:
    """Call service with invalid input and check that it raises."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        assert await async_setup_component(hass, "hassio", {})
        await hass.async_block_till_done()

    with pytest.raises(Invalid):
        await hass.services.async_call(
            "hassio", f"{app_or_addon}_start", {app_or_addon: "inv@lid"}
        )
    with pytest.raises(Invalid):
        await hass.services.async_call(
            "hassio",
            f"{app_or_addon}_stdin",
            {app_or_addon: "inv@lid", "input": "test"},
        )


@pytest.mark.parametrize(
    ("service", "service_data"),
    [
        (
            "backup_partial",
            {"apps": ["test"], "addons": ["test"]},
        ),
        (
            "restore_partial",
            {"apps": ["test"], "addons": ["test"], "slug": "test"},
        ),
    ],
)
@pytest.mark.usefixtures("addon_installed")
async def test_service_calls_apps_addons_exclusive(
    hass: HomeAssistant,
    supervisor_is_connected: AsyncMock,
    service: str,
    service_data: dict[str, Any],
) -> None:
    """Test that apps and addons parameters are mutually exclusive."""
    supervisor_is_connected.side_effect = SupervisorError
    with patch.dict(os.environ, MOCK_ENVIRON):
        assert await async_setup_component(hass, "hassio", {})
        await hass.async_block_till_done()

    with pytest.raises(
        Invalid, match="two or more values in the same group of exclusion"
    ):
        await hass.services.async_call("hassio", service, service_data)


@pytest.mark.parametrize(
    "app_or_addon",
    ["app", "addon"],
)
async def test_addon_service_call_with_complex_slug(
    hass: HomeAssistant,
    supervisor_is_connected: AsyncMock,
    app_or_addon: str,
    addons_list: AsyncMock,
) -> None:
    """Addon slugs can have ., - and _, confirm that passes validation."""
    addons_list.return_value = [
        InstalledAddon(
            detached=False,
            advanced=False,
            available=True,
            build=False,
            description="",
            homeassistant=None,
            icon=False,
            logo=False,
            name="test.a_1-2",
            repository="core",
            slug="test.a_1-2",
            stage=AddonStage.STABLE,
            update_available=False,
            url="https://github.com",
            version_latest="1.0.0",
            version="1.0.0",
            state=AddonState.STOPPED,
        )
    ]
    supervisor_is_connected.side_effect = SupervisorError
    with patch.dict(os.environ, MOCK_ENVIRON):
        assert await async_setup_component(hass, "hassio", {})
        await hass.async_block_till_done()

    await hass.services.async_call(
        "hassio", f"{app_or_addon}_start", {app_or_addon: "test.a_1-2"}
    )


@pytest.mark.usefixtures("all_setup_requests")
async def test_service_calls_core(
    hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
    """Call core service and check the API calls behind that."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        assert await async_setup_component(hass, "homeassistant", {})
        assert await async_setup_component(hass, "hassio", {})

    await hass.services.async_call("homeassistant", "stop")
    await hass.async_block_till_done()

    supervisor_client.homeassistant.stop.assert_called_once_with()

    await hass.services.async_call("homeassistant", "check_config")
    await hass.async_block_till_done()

    with patch(
        "homeassistant.config.async_check_ha_config_file", return_value=None
    ) as mock_check_config:
        await hass.services.async_call("homeassistant", "restart")
        await hass.async_block_till_done()
        assert mock_check_config.called

    supervisor_client.homeassistant.restart.assert_called_once_with()


@pytest.mark.parametrize(
    "app_or_addon",
    ["apps", "addons"],
)
@pytest.mark.usefixtures("hassio_env", "supervisor_client")
async def test_invalid_service_calls_app_duplicates(
    hass: HomeAssistant, app_or_addon: str
) -> None:
    """Test invalid backup/restore service calls due to duplicates in apps list."""
    assert await async_setup_component(hass, "hassio", {})

    with pytest.raises(Invalid, match="contains duplicate items"):
        await hass.services.async_call(
            "hassio", "backup_partial", {app_or_addon: ["test", "test"]}
        )

    with pytest.raises(Invalid, match="contains duplicate items"):
        await hass.services.async_call(
            "hassio", "restore_partial", {app_or_addon: ["test", "test"]}
        )


@pytest.mark.usefixtures("hassio_env", "supervisor_client")
async def test_invalid_service_calls_folder_duplicates(hass: HomeAssistant) -> None:
    """Test invalid backup/restore service calls due to duplicates in folder list."""
    assert await async_setup_component(hass, "hassio", {})

    with pytest.raises(Invalid, match="contains duplicate items"):
        await hass.services.async_call(
            "hassio", "backup_partial", {"folders": ["ssl", "ssl"]}
        )

    with pytest.raises(Invalid, match="contains duplicate items"):
        await hass.services.async_call(
            "hassio", "restore_partial", {"folders": ["ssl", "ssl"]}
        )


@pytest.mark.usefixtures("hassio_env")
async def test_partial_backup_legacy_homeassistant_folder(
    hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
    """Test legacy "homeassistant" folder is translated to homeassistant=True."""
    assert await async_setup_component(hass, "hassio", {})
    supervisor_client.backups.partial_backup.return_value = NewBackup(
        job_id=uuid4(), slug="partial"
    )

    await hass.services.async_call(
        "hassio",
        "backup_partial",
        {"folders": ["homeassistant", "ssl"], "name": "test"},
        blocking=True,
    )
    supervisor_client.backups.partial_backup.assert_called_once_with(
        PartialBackupOptions(
            name="test",
            homeassistant=True,
            folders={Folder.SSL},
        )
    )
    issue_registry = ir.async_get(hass)
    assert (
        issue_registry.async_get_issue("hassio", "legacy_homeassistant_folder")
        is not None
    )


@pytest.mark.usefixtures("hassio_env")
async def test_partial_restore_legacy_homeassistant_folder(
    hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
    """Test that the legacy "homeassistant" folder is translated for restore too."""
    assert await async_setup_component(hass, "hassio", {})

    await hass.services.async_call(
        "hassio",
        "restore_partial",
        {"slug": "test", "folders": ["homeassistant", "ssl"]},
        blocking=True,
    )
    supervisor_client.backups.partial_restore.assert_called_once_with(
        "test",
        PartialRestoreOptions(
            homeassistant=True,
            folders={Folder.SSL},
        ),
    )


@pytest.mark.usefixtures("hassio_env", "supervisor_client")
async def test_partial_backup_invalid_folder(hass: HomeAssistant) -> None:
    """Test that an unknown folder name is rejected."""
    assert await async_setup_component(hass, "hassio", {})

    with pytest.raises(Invalid, match="not a valid value"):
        await hass.services.async_call(
            "hassio", "backup_partial", {"folders": ["bogus"]}
        )


@pytest.mark.usefixtures("hassio_env", "supervisor_client")
async def test_partial_backup_legacy_homeassistant_folder_conflict(
    hass: HomeAssistant,
) -> None:
    """Reject combining homeassistant=False with the legacy "homeassistant" folder."""
    assert await async_setup_component(hass, "hassio", {})

    with pytest.raises(ServiceValidationError, match="conflicts"):
        await hass.services.async_call(
            "hassio",
            "backup_partial",
            {"homeassistant": False, "folders": ["homeassistant"]},
            blocking=True,
        )


@pytest.mark.usefixtures("addon_installed")
async def test_entry_load_and_unload(hass: HomeAssistant) -> None:
    """Test loading and unloading config entry."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    assert SENSOR_DOMAIN in hass.config.components
    assert BINARY_SENSOR_DOMAIN in hass.config.components
    assert ADDONS_COORDINATOR in hass.data

    assert await hass.config_entries.async_unload(config_entry.entry_id)
    await hass.async_block_till_done()
    assert ADDONS_COORDINATOR not in hass.data


async def test_migration_off_hassio(hass: HomeAssistant) -> None:
    """Test when a user moves instance off Hass.io, config entry gets cleaned up."""
    config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
    config_entry.add_to_hass(hass)
    assert not await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()
    assert hass.config_entries.async_entries(DOMAIN) == []


@pytest.mark.usefixtures("addon_installed", "supervisor_info")
async def test_device_registry_calls(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    addons_list: AsyncMock,
    os_info: AsyncMock,
) -> None:
    """Test device registry entries for hassio."""
    addons_list.return_value[0] = replace(
        addons_list.return_value[0],
        version="1.0.0",
        version_latest="1.0.0",
        update_available=False,
    )
    addons_list.return_value[1] = replace(
        addons_list.return_value[1],
        version="1.0.0",
        version_latest="1.0.0",
        state=AddonState.STARTED,
    )
    os_info.return_value = replace(
        os_info.return_value,
        board="odroid-n2",
        boot="A",
        version="5.12",
        version_latest="5.12",
    )

    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done(wait_background_tasks=True)
        assert len(device_registry.devices) == 6

    addons_list.return_value.pop(0)

    # Test that when addon is removed, next update will remove the
    # add-on and subsequent updates won't
    async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1))
    await hass.async_block_till_done(wait_background_tasks=True)
    assert len(device_registry.devices) == 5

    async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2))
    await hass.async_block_till_done(wait_background_tasks=True)
    assert len(device_registry.devices) == 5

    addons_list.return_value.append(
        InstalledAddon(
            detached=False,
            advanced=False,
            available=True,
            build=False,
            description="",
            homeassistant=None,
            icon=False,
            logo=False,
            name="test3",
            repository="core",
            slug="test3",
            stage=AddonStage.STABLE,
            update_available=False,
            url="https://github.com",
            version_latest="1.0.0",
            version="1.0.0",
            state=AddonState.STOPPED,
        )
    )

    # Test that when addon is added, next update will reload the entry so we register
    # a new device
    async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3))
    await hass.async_block_till_done()
    assert len(device_registry.devices) == 5


@pytest.mark.usefixtures("addon_installed")
async def test_coordinator_updates(
    hass: HomeAssistant, caplog: pytest.LogCaptureFixture, supervisor_client: AsyncMock
) -> None:
    """Test coordinator updates."""
    await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

        # Initial refresh, no update refresh call
        supervisor_client.reload_updates.assert_not_called()

    async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20))
    await hass.async_block_till_done(wait_background_tasks=True)

    # Scheduled refresh, no update refresh call
    supervisor_client.reload_updates.assert_not_called()

    await hass.services.async_call(
        HOMEASSISTANT_DOMAIN,
        SERVICE_UPDATE_ENTITY,
        {
            "entity_id": [
                "update.home_assistant_core_update",
                "update.home_assistant_supervisor_update",
            ]
        },
        blocking=True,
    )

    # There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
    supervisor_client.reload_updates.assert_not_called()
    async_fire_time_changed(
        hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
    )
    await hass.async_block_till_done(wait_background_tasks=True)
    supervisor_client.reload_updates.assert_called_once()

    supervisor_client.reload_updates.reset_mock()
    supervisor_client.reload_updates.side_effect = SupervisorError("Unknown")
    await hass.services.async_call(
        HOMEASSISTANT_DOMAIN,
        SERVICE_UPDATE_ENTITY,
        {
            "entity_id": [
                "update.home_assistant_core_update",
                "update.home_assistant_supervisor_update",
            ]
        },
        blocking=True,
    )
    # There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
    async_fire_time_changed(
        hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
    )
    await hass.async_block_till_done()
    supervisor_client.reload_updates.assert_called_once()
    assert "Error on Supervisor API: Unknown" in caplog.text


@pytest.mark.usefixtures("entity_registry_enabled_by_default", "addon_installed")
async def test_coordinator_updates_stats_entities_enabled(
    hass: HomeAssistant,
    caplog: pytest.LogCaptureFixture,
    supervisor_client: AsyncMock,
) -> None:
    """Test coordinator updates with stats entities enabled."""
    await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()
        # Initial refresh without stats
        supervisor_client.reload_updates.assert_not_called()

        # Stats entities trigger refresh on the stats coordinator,
        # which does not call reload_updates
        async_fire_time_changed(
            hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
        )
        await hass.async_block_till_done()

        supervisor_client.reload_updates.assert_not_called()

    async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20))
    await hass.async_block_till_done()
    supervisor_client.reload_updates.assert_not_called()

    await hass.services.async_call(
        HOMEASSISTANT_DOMAIN,
        SERVICE_UPDATE_ENTITY,
        {
            "entity_id": [
                "update.home_assistant_core_update",
                "update.home_assistant_supervisor_update",
            ]
        },
        blocking=True,
    )
    supervisor_client.reload_updates.assert_not_called()

    # There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
    async_fire_time_changed(
        hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
    )
    await hass.async_block_till_done()

    supervisor_client.reload_updates.reset_mock()
    supervisor_client.reload_updates.side_effect = SupervisorError("Unknown")
    await hass.services.async_call(
        HOMEASSISTANT_DOMAIN,
        SERVICE_UPDATE_ENTITY,
        {
            "entity_id": [
                "update.home_assistant_core_update",
                "update.home_assistant_supervisor_update",
            ]
        },
        blocking=True,
    )
    # There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
    async_fire_time_changed(
        hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
    )
    await hass.async_block_till_done()
    supervisor_client.reload_updates.assert_called_once()
    assert "Error on Supervisor API: Unknown" in caplog.text


@pytest.mark.parametrize(
    ("board", "integration"),
    [
        ("green", "homeassistant_green"),
        ("odroid-c2", "hardkernel"),
        ("odroid-c4", "hardkernel"),
        ("odroid-n2", "hardkernel"),
        ("odroid-xu4", "hardkernel"),
        ("rpi2", "raspberry_pi"),
        ("rpi3", "raspberry_pi"),
        ("rpi3-64", "raspberry_pi"),
        ("rpi4", "raspberry_pi"),
        ("rpi4-64", "raspberry_pi"),
        ("yellow", "homeassistant_yellow"),
    ],
)
async def test_setup_hardware_integration(
    hass: HomeAssistant,
    supervisor_client: AsyncMock,
    os_info: AsyncMock,
    board: str,
    integration: str,
) -> None:
    """Test setup initiates hardware integration."""
    os_info.return_value = replace(os_info.return_value, board=board)

    with (
        patch.dict(os.environ, MOCK_ENVIRON),
        patch(
            f"homeassistant.components.{integration}.async_setup_entry",
            return_value=True,
        ) as mock_setup_entry,
        patch(
            "homeassistant.components.homeassistant_yellow.config_flow.probe_silabs_firmware_info",
            return_value=None,
        ),
    ):
        result = await async_setup_component(hass, "hassio", {"hassio": {}})
        await hass.async_block_till_done(wait_background_tasks=True)

    assert result
    assert len(supervisor_client.mock_calls) == 16
    assert len(mock_setup_entry.mock_calls) == 1


def test_hostname_from_addon_slug() -> None:
    """Test hostname_from_addon_slug."""
    assert hostname_from_addon_slug("mqtt") == "mqtt"
    assert (
        hostname_from_addon_slug("core_silabs_multiprotocol")
        == "core-silabs-multiprotocol"
    )


@pytest.mark.parametrize(
    ("board", "issue_id"),
    [
        ("rpi3", "deprecated_os_aarch64"),
        ("rpi4", "deprecated_os_aarch64"),
        ("tinker", "deprecated_os_armv7"),
        ("odroid-xu4", "deprecated_os_armv7"),
        ("rpi2", "deprecated_os_armv7"),
    ],
)
async def test_deprecated_installation_issue_os_armv7(
    hass: HomeAssistant,
    issue_registry: ir.IssueRegistry,
    freezer: FrozenDateTimeFactory,
    board: str,
    issue_id: str,
) -> None:
    """Test deprecated installation issue."""
    with (
        patch.dict(os.environ, MOCK_ENVIRON),
        patch(
            "homeassistant.components.hassio._is_32_bit",
            return_value=True,
        ),
        patch(
            "homeassistant.components.hassio.get_os_info", return_value={"board": board}
        ),
        patch(
            "homeassistant.components.hassio.get_info",
            return_value={"hassos": True, "arch": "armv7"},
        ),
        patch("homeassistant.components.hardware.async_setup", return_value=True),
    ):
        assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()
        freezer.tick(REQUEST_REFRESH_DELAY)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()
        await hass.services.async_call(
            HOMEASSISTANT_DOMAIN,
            SERVICE_UPDATE_ENTITY,
            {
                "entity_id": [
                    "update.home_assistant_core_update",
                    "update.home_assistant_supervisor_update",
                ]
            },
            blocking=True,
        )
        freezer.tick(HASSIO_MAIN_UPDATE_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()

    assert len(issue_registry.issues) == 1
    issue = issue_registry.async_get_issue("homeassistant", issue_id)
    assert issue.domain == "homeassistant"
    assert issue.severity == ir.IssueSeverity.WARNING
    assert issue.translation_placeholders == {
        "installation_guide": "https://www.home-assistant.io/installation/",
    }


@pytest.mark.parametrize(
    "arch",
    [
        "i386",
        "armhf",
        "armv7",
    ],
)
async def test_deprecated_installation_issue_32bit_os(
    hass: HomeAssistant,
    issue_registry: ir.IssueRegistry,
    freezer: FrozenDateTimeFactory,
    arch: str,
) -> None:
    """Test deprecated architecture issue."""
    with (
        patch.dict(os.environ, MOCK_ENVIRON),
        patch(
            "homeassistant.components.hassio._is_32_bit",
            return_value=True,
        ),
        patch(
            "homeassistant.components.hassio.get_os_info",
            return_value={"board": "rpi3-64"},
        ),
        patch(
            "homeassistant.components.hassio.get_info",
            return_value={"hassos": True, "arch": arch},
        ),
        patch("homeassistant.components.hardware.async_setup", return_value=True),
    ):
        assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()
        freezer.tick(REQUEST_REFRESH_DELAY)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()
        await hass.services.async_call(
            HOMEASSISTANT_DOMAIN,
            SERVICE_UPDATE_ENTITY,
            {
                "entity_id": [
                    "update.home_assistant_core_update",
                    "update.home_assistant_supervisor_update",
                ]
            },
            blocking=True,
        )
        freezer.tick(HASSIO_MAIN_UPDATE_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()

    assert len(issue_registry.issues) == 1
    issue = issue_registry.async_get_issue("homeassistant", "deprecated_architecture")
    assert issue.domain == "homeassistant"
    assert issue.severity == ir.IssueSeverity.WARNING
    assert issue.translation_placeholders == {"installation_type": "OS", "arch": arch}


@pytest.mark.parametrize(
    "arch",
    [
        "i386",
        "armhf",
        "armv7",
    ],
)
async def test_deprecated_installation_issue_32bit_supervised(
    hass: HomeAssistant,
    issue_registry: ir.IssueRegistry,
    freezer: FrozenDateTimeFactory,
    arch: str,
) -> None:
    """Test deprecated architecture issue."""
    with (
        patch.dict(os.environ, MOCK_ENVIRON),
        patch(
            "homeassistant.components.hassio._is_32_bit",
            return_value=True,
        ),
        patch(
            "homeassistant.components.hassio.get_os_info",
            return_value={"board": "rpi3-64"},
        ),
        patch(
            "homeassistant.components.hassio.get_info",
            return_value={"hassos": None, "arch": arch},
        ),
        patch("homeassistant.components.hardware.async_setup", return_value=True),
    ):
        assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()
        freezer.tick(REQUEST_REFRESH_DELAY)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()
        await hass.services.async_call(
            HOMEASSISTANT_DOMAIN,
            SERVICE_UPDATE_ENTITY,
            {
                "entity_id": [
                    "update.home_assistant_core_update",
                    "update.home_assistant_supervisor_update",
                ]
            },
            blocking=True,
        )
        freezer.tick(HASSIO_MAIN_UPDATE_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()

    assert len(issue_registry.issues) == 1
    issue = issue_registry.async_get_issue(
        "homeassistant", "deprecated_method_architecture"
    )
    assert issue.domain == "homeassistant"
    assert issue.severity == ir.IssueSeverity.WARNING
    assert issue.translation_placeholders == {
        "installation_type": "Supervised",
        "arch": arch,
    }


@pytest.mark.parametrize(
    "arch",
    [
        "amd64",
        "aarch64",
    ],
)
async def test_deprecated_installation_issue_64bit_supervised(
    hass: HomeAssistant,
    issue_registry: ir.IssueRegistry,
    freezer: FrozenDateTimeFactory,
    arch: str,
) -> None:
    """Test deprecated architecture issue."""
    with (
        patch.dict(os.environ, MOCK_ENVIRON),
        patch(
            "homeassistant.components.hassio._is_32_bit",
            return_value=False,
        ),
        patch(
            "homeassistant.components.hassio.get_os_info",
            return_value={"board": "generic-x86-64"},
        ),
        patch(
            "homeassistant.components.hassio.get_info",
            return_value={"hassos": None, "arch": arch},
        ),
        patch("homeassistant.components.hardware.async_setup", return_value=True),
    ):
        assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()
        freezer.tick(REQUEST_REFRESH_DELAY)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()
        await hass.services.async_call(
            HOMEASSISTANT_DOMAIN,
            SERVICE_UPDATE_ENTITY,
            {
                "entity_id": [
                    "update.home_assistant_core_update",
                    "update.home_assistant_supervisor_update",
                ]
            },
            blocking=True,
        )
        freezer.tick(HASSIO_MAIN_UPDATE_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()

    assert len(issue_registry.issues) == 1
    issue = issue_registry.async_get_issue("homeassistant", "deprecated_method")
    assert issue.domain == "homeassistant"
    assert issue.severity == ir.IssueSeverity.WARNING
    assert issue.translation_placeholders == {
        "installation_type": "Supervised",
        "arch": arch,
    }


@pytest.mark.parametrize(
    ("board", "issue_id"),
    [
        ("rpi5", "deprecated_os_aarch64"),
    ],
)
async def test_deprecated_installation_issue_supported_board(
    hass: HomeAssistant,
    issue_registry: ir.IssueRegistry,
    freezer: FrozenDateTimeFactory,
    board: str,
    issue_id: str,
) -> None:
    """Test no deprecated installation issue for a supported board."""
    with (
        patch.dict(os.environ, MOCK_ENVIRON),
        patch(
            "homeassistant.components.hassio._is_32_bit",
            return_value=False,
        ),
        patch(
            "homeassistant.components.hassio.get_os_info", return_value={"board": board}
        ),
        patch(
            "homeassistant.components.hassio.get_info",
            return_value={"hassos": True, "arch": "aarch64"},
        ),
    ):
        assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()
        freezer.tick(REQUEST_REFRESH_DELAY)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()
        await hass.services.async_call(
            HOMEASSISTANT_DOMAIN,
            SERVICE_UPDATE_ENTITY,
            {
                "entity_id": [
                    "update.home_assistant_core_update",
                    "update.home_assistant_supervisor_update",
                ]
            },
            blocking=True,
        )
        freezer.tick(HASSIO_MAIN_UPDATE_INTERVAL)
        async_fire_time_changed(hass)
        await hass.async_block_till_done()

    assert len(issue_registry.issues) == 0


async def mount_reload_test_setup(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    supervisor_client: AsyncMock,
) -> dr.DeviceEntry:
    """Set up mount reload test and return the device entry."""
    supervisor_client.mounts.info = AsyncMock(
        return_value=MountsInfo(
            default_backup_mount=None,
            mounts=[
                CIFSMountResponse(
                    share="files",
                    server="1.2.3.4",
                    name="NAS",
                    type=MountType.CIFS,
                    usage=MountUsage.SHARE,
                    read_only=False,
                    state=MountState.ACTIVE,
                    user_path=PurePath("/share/nas"),
                )
            ],
        )
    )

    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    device = device_registry.async_get_device(identifiers={(DOMAIN, "mount_NAS")})
    assert device is not None
    return device


async def test_mount_reload_action(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    supervisor_client: AsyncMock,
) -> None:
    """Test reload_mount service call."""
    device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
    await hass.services.async_call(
        "hassio", "mount_reload", {"device_id": device.id}, blocking=True
    )
    supervisor_client.mounts.reload_mount.assert_awaited_once_with("NAS")


async def test_mount_reload_action_failure(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    supervisor_client: AsyncMock,
) -> None:
    """Test reload_mount service call failure."""
    device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
    supervisor_client.mounts.reload_mount = AsyncMock(
        side_effect=SupervisorError("test failure")
    )
    with pytest.raises(HomeAssistantError) as exc:
        await hass.services.async_call(
            "hassio", "mount_reload", {"device_id": device.id}, blocking=True
        )
    assert str(exc.value) == "Failed to reload mount NAS: test failure"


async def test_mount_reload_unknown_device_id(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    supervisor_client: AsyncMock,
) -> None:
    """Test reload_mount with unknown device ID."""
    await mount_reload_test_setup(hass, device_registry, supervisor_client)
    with pytest.raises(ServiceValidationError) as exc:
        await hass.services.async_call(
            "hassio", "mount_reload", {"device_id": "1234"}, blocking=True
        )
    assert str(exc.value) == "Device ID not found"


async def test_mount_reload_no_name(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    supervisor_client: AsyncMock,
) -> None:
    """Test reload_mount with an unnamed device."""
    device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
    device_registry.async_update_device(device.id, name=None)
    with pytest.raises(ServiceValidationError) as exc:
        await hass.services.async_call(
            "hassio", "mount_reload", {"device_id": device.id}, blocking=True
        )
    assert str(exc.value) == "Device is not a supervisor mount point"


async def test_mount_reload_invalid_model(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    supervisor_client: AsyncMock,
) -> None:
    """Test reload_mount with an invalid model."""
    device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
    device_registry.async_update_device(device.id, model=None)
    with pytest.raises(ServiceValidationError) as exc:
        await hass.services.async_call(
            "hassio", "mount_reload", {"device_id": device.id}, blocking=True
        )
    assert str(exc.value) == "Device is not a supervisor mount point"


async def test_mount_reload_not_supervisor_device(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    supervisor_client: AsyncMock,
) -> None:
    """Test reload_mount with a device not belonging to the supervisor."""
    device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
    config_entry = MockConfigEntry()
    config_entry.add_to_hass(hass)
    device2 = device_registry.async_get_or_create(
        config_entry_id=config_entry.entry_id,
        identifiers={("test", "test")},
        name=device.name,
        model=device.model,
    )
    with pytest.raises(ServiceValidationError) as exc:
        await hass.services.async_call(
            "hassio", "mount_reload", {"device_id": device2.id}, blocking=True
        )
    assert str(exc.value) == "Device is not a supervisor mount point"


async def test_mount_reload_selector_matches_device_name(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    supervisor_client: AsyncMock,
) -> None:
    """Test that the model name in the selector of mount reload is valid."""
    device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
    services = load_yaml_dict(f"{hassio.__path__[0]}/services.yaml")
    assert (
        services["mount_reload"]["fields"]["device_id"]["selector"]["device"]["filter"][
            "model"
        ]
        == device.model
    )


@pytest.mark.usefixtures("mock_all")
async def test_get_supervisor_info(hass: HomeAssistant) -> None:
    """Test get_supervisor_info returns a dict with backwards-compat keys."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    result = get_supervisor_info(hass)
    assert isinstance(result, dict)
    # Deprecated backwards-compat keys folded in from store/addons data
    assert "repositories" in result
    assert isinstance(result["repositories"], list)
    assert "addons" in result
    assert isinstance(result["addons"], list)
    assert all(isinstance(addon, dict) for addon in result["addons"])


@pytest.mark.usefixtures("mock_all")
async def test_get_info(hass: HomeAssistant) -> None:
    """Test get_info returns serialized dict with expected values."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    result = get_info(hass)
    assert isinstance(result, dict)
    assert result["supervisor"] == "222"
    assert result["homeassistant"] == "0.110.0"
    assert result["hassos"] == "1.2.3"


@pytest.mark.usefixtures("mock_all")
async def test_get_host_info(hass: HomeAssistant) -> None:
    """Test get_host_info returns serialized dict with expected values."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    result = get_host_info(hass)
    assert isinstance(result, dict)
    assert result["chassis"] == "vm"
    assert result["disk_total"] == 100.0
    assert result["kernel"] == "4.19.0-6-amd64"


@pytest.mark.usefixtures("mock_all")
async def test_get_store(hass: HomeAssistant) -> None:
    """Test get_store returns serialized dict with expected values."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    result = get_store(hass)
    assert isinstance(result, dict)
    assert "addons" in result
    assert "repositories" in result
    assert isinstance(result["addons"], list)
    assert isinstance(result["repositories"], list)


@pytest.mark.usefixtures("mock_all")
async def test_get_network_info(hass: HomeAssistant) -> None:
    """Test get_network_info returns serialized dict with expected values."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    result = get_network_info(hass)
    assert isinstance(result, dict)
    assert result["host_internet"] is True
    assert result["supervisor_internet"] is True
    assert isinstance(result["interfaces"], list)


@pytest.mark.usefixtures("mock_all")
async def test_get_addons_info(hass: HomeAssistant) -> None:
    """Test get_addons_info returns serialized dicts, not model objects."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    result = get_addons_info(hass)
    assert isinstance(result, dict)
    assert "test" in result
    assert isinstance(result["test"], dict)
    assert result["test"]["slug"] == "test"
    assert result["test"]["version"] == "1.0.0"
    assert result["test"]["hassio_api"] is False
    assert result["test"]["supervisor_api"] is False
    assert result["test"]["hassio_role"] == "default"
    assert result["test"]["supervisor_role"] == "default"


@pytest.mark.usefixtures("mock_all")
async def test_get_addons_list(hass: HomeAssistant) -> None:
    """Test get_addons_list returns a list of serialized dicts."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    result = get_addons_list(hass)
    assert isinstance(result, list)
    assert all(isinstance(addon, dict) for addon in result)
    slugs = {addon["slug"] for addon in result}
    assert "test" in slugs
    assert "test2" in slugs


@pytest.mark.usefixtures("mock_all", "entity_registry_enabled_by_default")
async def test_get_addons_stats(hass: HomeAssistant) -> None:
    """Test get_addons_stats returns serialized dicts, not model objects.

    Both test addons are STOPPED in mock_all so no addon stats are fetched;
    the result is an empty dict which is the correct return type.
    """
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    result = get_addons_stats(hass)
    assert isinstance(result, dict)
    # All values must be plain dicts, never AddonsStats model objects
    for stats in result.values():
        assert isinstance(stats, dict)


@pytest.mark.usefixtures("mock_all", "entity_registry_enabled_by_default")
async def test_get_core_stats(hass: HomeAssistant) -> None:
    """Test get_core_stats returns serialized dict with expected values."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    # Stats entities subscribe during setup and trigger a debounced refresh
    async_fire_time_changed(
        hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
    )
    await hass.async_block_till_done()

    result = get_core_stats(hass)
    assert isinstance(result, dict)
    assert result["cpu_percent"] == 0.99
    assert result["memory_percent"] == 4.59


@pytest.mark.usefixtures("mock_all", "entity_registry_enabled_by_default")
async def test_get_supervisor_stats(hass: HomeAssistant) -> None:
    """Test get_supervisor_stats returns serialized dict with expected values."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    # Stats entities subscribe during setup and trigger a debounced refresh
    async_fire_time_changed(
        hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
    )
    await hass.async_block_till_done()

    result = get_supervisor_stats(hass)
    assert isinstance(result, dict)
    assert result["cpu_percent"] == 0.99
    assert result["memory_percent"] == 4.59


@pytest.mark.usefixtures("mock_all")
async def test_get_os_info(hass: HomeAssistant) -> None:
    """Test get_os_info returns serialized dict with expected values."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    result = get_os_info(hass)
    assert isinstance(result, dict)
    assert result["version"] == "1.0.0"
    assert result["version_latest"] == "1.0.0"
    assert result["update_available"] is False


@pytest.mark.usefixtures("mock_all")
async def test_get_core_info(hass: HomeAssistant) -> None:
    """Test get_core_info returns serialized dict with expected values."""
    with patch.dict(os.environ, MOCK_ENVIRON):
        config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
        config_entry.add_to_hass(hass)
        assert await hass.config_entries.async_setup(config_entry.entry_id)
        await hass.async_block_till_done()

    result = get_core_info(hass)
    assert isinstance(result, dict)
    assert result["version"] == "1.0.0"
    assert result["version_latest"] == "1.0.0"
    assert result["image"] == "homeassistant"


@pytest.mark.usefixtures("all_setup_requests")
async def test_stop_handler_restored_on_unload(
    hass: HomeAssistant,
    supervisor_client: AsyncMock,
) -> None:
    """Test that the default stop handler is restored when the hassio entry unloads."""
    assert await async_setup_component(hass, "homeassistant", {})
    with patch.dict(os.environ, MOCK_ENVIRON):
        assert await async_setup_component(hass, "hassio", {})
        await hass.async_block_till_done()

    entry = hass.config_entries.async_entries("hassio")[0]
    await hass.config_entries.async_unload(entry.entry_id)

    # After a stop call to core no longer calls supervisor
    with patch.object(hass, "async_stop") as mock_stop:
        await hass.services.async_call("homeassistant", "stop", {})
        await hass.async_block_till_done()
        mock_stop.assert_called_once()
        supervisor_client.homeassistant.stop.assert_not_called()


@pytest.mark.usefixtures("supervisor_client")
async def test_supervisor_issues_not_set_on_coordinator_failure(
    hass: HomeAssistant,
    supervisor_is_connected: AsyncMock,
    supervisor_root_info: AsyncMock,
) -> None:
    """Test DATA_KEY_SUPERVISOR_ISSUES is not populated when coordinator fails.

    If a coordinator first-refresh raises ConfigEntryNotReady the issues
    listener must not be registered, preventing accumulation across retries.
    """
    supervisor_root_info.side_effect = SupervisorError()
    with patch.dict(os.environ, MOCK_ENVIRON):
        result = await async_setup_component(hass, "hassio", {})
        assert result

    entry = hass.config_entries.async_entries("hassio")[0]
    assert entry.state is ConfigEntryState.SETUP_RETRY
    assert DATA_KEY_SUPERVISOR_ISSUES not in hass.data
