"""Test the Matter integration init."""
from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Callable, Generator
from unittest.mock import AsyncMock, MagicMock, call, patch

from aiohttp import ClientWebSocketResponse
from matter_server.client.exceptions import CannotConnect, InvalidServerVersion
from matter_server.common.helpers.util import dataclass_from_dict
from matter_server.common.models.error import MatterError
from matter_server.common.models.node import MatterNode
import pytest

from homeassistant.components.hassio import HassioAPIError
from homeassistant.components.matter.const import DOMAIN
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
    device_registry as dr,
    entity_registry as er,
    issue_registry as ir,
)
from homeassistant.setup import async_setup_component

from .common import load_and_parse_node_fixture, setup_integration_with_node_fixture

from tests.common import MockConfigEntry


@pytest.fixture(name="connect_timeout")
def connect_timeout_fixture() -> Generator[int, None, None]:
    """Mock the connect timeout."""
    with patch("homeassistant.components.matter.CONNECT_TIMEOUT", new=0) as timeout:
        yield timeout


@pytest.fixture(name="listen_ready_timeout")
def listen_ready_timeout_fixture() -> Generator[int, None, None]:
    """Mock the listen ready timeout."""
    with patch(
        "homeassistant.components.matter.LISTEN_READY_TIMEOUT", new=0
    ) as timeout:
        yield timeout


async def test_entry_setup_unload(
    hass: HomeAssistant,
    matter_client: MagicMock,
) -> None:
    """Test the integration set up and unload."""
    node_data = load_and_parse_node_fixture("onoff-light")
    node = dataclass_from_dict(
        MatterNode,
        node_data,
    )
    matter_client.get_nodes.return_value = [node]
    matter_client.get_node.return_value = node
    entry = MockConfigEntry(domain="matter", data={"url": "ws://localhost:5580/ws"})
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert matter_client.connect.call_count == 1
    assert entry.state == ConfigEntryState.LOADED
    entity_state = hass.states.get("light.mock_onoff_light")
    assert entity_state
    assert entity_state.state != STATE_UNAVAILABLE

    await hass.config_entries.async_unload(entry.entry_id)

    assert matter_client.disconnect.call_count == 1
    assert entry.state == ConfigEntryState.NOT_LOADED
    entity_state = hass.states.get("light.mock_onoff_light")
    assert entity_state
    assert entity_state.state == STATE_UNAVAILABLE


async def test_home_assistant_stop(
    hass: HomeAssistant,
    matter_client: MagicMock,
    integration: MockConfigEntry,
) -> None:
    """Test clean up on home assistant stop."""
    await hass.async_stop()

    assert matter_client.disconnect.call_count == 1


@pytest.mark.parametrize("error", [CannotConnect(Exception("Boom")), Exception("Boom")])
async def test_connect_failed(
    hass: HomeAssistant,
    matter_client: MagicMock,
    error: Exception,
) -> None:
    """Test failure during client connection."""
    entry = MockConfigEntry(domain=DOMAIN, data={"url": "ws://localhost:5580/ws"})
    entry.add_to_hass(hass)
    matter_client.connect.side_effect = error

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.SETUP_RETRY


async def test_connect_timeout(
    hass: HomeAssistant,
    matter_client: MagicMock,
    connect_timeout: int,
) -> None:
    """Test timeout during client connection."""
    entry = MockConfigEntry(domain=DOMAIN, data={"url": "ws://localhost:5580/ws"})
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.SETUP_RETRY


@pytest.mark.parametrize("error", [MatterError("Boom"), Exception("Boom")])
async def test_listen_failure_timeout(
    hass: HomeAssistant,
    listen_ready_timeout: int,
    matter_client: MagicMock,
    error: Exception,
) -> None:
    """Test client listen errors during the first timeout phase."""

    async def start_listening(listen_ready: asyncio.Event) -> None:
        """Mock the client start_listening method."""
        # Set the connect side effect to stop an endless loop on reload.
        matter_client.connect.side_effect = MatterError("Boom")
        raise error

    matter_client.start_listening.side_effect = start_listening
    entry = MockConfigEntry(domain=DOMAIN, data={"url": "ws://localhost:5580/ws"})
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.SETUP_RETRY


@pytest.mark.parametrize("error", [MatterError("Boom"), Exception("Boom")])
async def test_listen_failure_config_entry_not_loaded(
    hass: HomeAssistant,
    matter_client: MagicMock,
    error: Exception,
) -> None:
    """Test client listen errors during the final phase before config entry loaded."""
    listen_block = asyncio.Event()

    async def start_listening(listen_ready: asyncio.Event) -> None:
        """Mock the client start_listening method."""
        listen_ready.set()
        await listen_block.wait()
        # Set the connect side effect to stop an endless loop on reload.
        matter_client.connect.side_effect = MatterError("Boom")
        raise error

    async def get_nodes() -> list[MagicMock]:
        """Mock the client get_nodes method."""
        listen_block.set()
        return []

    matter_client.start_listening.side_effect = start_listening
    matter_client.get_nodes.side_effect = get_nodes
    entry = MockConfigEntry(domain=DOMAIN, data={"url": "ws://localhost:5580/ws"})
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.SETUP_RETRY
    assert matter_client.disconnect.call_count == 1


@pytest.mark.parametrize("error", [MatterError("Boom"), Exception("Boom")])
async def test_listen_failure_config_entry_loaded(
    hass: HomeAssistant,
    matter_client: MagicMock,
    error: Exception,
) -> None:
    """Test client listen errors after config entry is loaded."""
    listen_block = asyncio.Event()

    async def start_listening(listen_ready: asyncio.Event) -> None:
        """Mock the client start_listening method."""
        listen_ready.set()
        await listen_block.wait()
        # Set the connect side effect to stop an endless loop on reload.
        matter_client.connect.side_effect = MatterError("Boom")
        raise error

    matter_client.start_listening.side_effect = start_listening
    entry = MockConfigEntry(domain=DOMAIN, data={"url": "ws://localhost:5580/ws"})
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state == ConfigEntryState.LOADED

    listen_block.set()
    await hass.async_block_till_done()

    assert entry.state == ConfigEntryState.SETUP_RETRY
    assert matter_client.disconnect.call_count == 1


async def test_raise_addon_task_in_progress(
    hass: HomeAssistant,
    addon_not_installed: AsyncMock,
    install_addon: AsyncMock,
    start_addon: AsyncMock,
) -> None:
    """Test raise ConfigEntryNotReady if an add-on task is in progress."""
    install_event = asyncio.Event()

    install_addon_original_side_effect = install_addon.side_effect

    async def install_addon_side_effect(hass: HomeAssistant, slug: str) -> None:
        """Mock install add-on."""
        await install_event.wait()
        await install_addon_original_side_effect(hass, slug)

    install_addon.side_effect = install_addon_side_effect

    entry = MockConfigEntry(
        domain=DOMAIN,
        title="Matter",
        data={
            "url": "ws://host1:5581/ws",
            "use_addon": True,
        },
    )
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await asyncio.sleep(0.05)

    assert entry.state is ConfigEntryState.SETUP_RETRY
    assert install_addon.call_count == 1
    assert start_addon.call_count == 0

    # Check that we only call install add-on once if a task is in progress.
    await hass.config_entries.async_reload(entry.entry_id)
    await asyncio.sleep(0.05)

    assert entry.state is ConfigEntryState.SETUP_RETRY
    assert install_addon.call_count == 1
    assert start_addon.call_count == 0

    install_event.set()
    await hass.async_block_till_done()

    assert install_addon.call_count == 1
    assert start_addon.call_count == 1


async def test_start_addon(
    hass: HomeAssistant,
    addon_installed: AsyncMock,
    addon_info: AsyncMock,
    install_addon: AsyncMock,
    start_addon: AsyncMock,
) -> None:
    """Test start the Matter Server add-on during entry setup."""
    entry = MockConfigEntry(
        domain=DOMAIN,
        title="Matter",
        data={
            "url": "ws://host1:5581/ws",
            "use_addon": True,
        },
    )
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.SETUP_RETRY
    assert addon_info.call_count == 1
    assert install_addon.call_count == 0
    assert start_addon.call_count == 1
    assert start_addon.call_args == call(hass, "core_matter_server")


async def test_install_addon(
    hass: HomeAssistant,
    addon_not_installed: AsyncMock,
    addon_store_info: AsyncMock,
    install_addon: AsyncMock,
    start_addon: AsyncMock,
) -> None:
    """Test install and start the Matter add-on during entry setup."""
    entry = MockConfigEntry(
        domain=DOMAIN,
        title="Matter",
        data={
            "url": "ws://host1:5581/ws",
            "use_addon": True,
        },
    )
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.SETUP_RETRY
    assert addon_store_info.call_count == 3
    assert install_addon.call_count == 1
    assert install_addon.call_args == call(hass, "core_matter_server")
    assert start_addon.call_count == 1
    assert start_addon.call_args == call(hass, "core_matter_server")


async def test_addon_info_failure(
    hass: HomeAssistant,
    addon_installed: AsyncMock,
    addon_info: AsyncMock,
    install_addon: AsyncMock,
    start_addon: AsyncMock,
) -> None:
    """Test failure to get add-on info for Matter add-on during entry setup."""
    addon_info.side_effect = HassioAPIError("Boom")
    entry = MockConfigEntry(
        domain=DOMAIN,
        title="Matter",
        data={
            "url": "ws://host1:5581/ws",
            "use_addon": True,
        },
    )
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.SETUP_RETRY
    assert addon_info.call_count == 1
    assert install_addon.call_count == 0
    assert start_addon.call_count == 0


@pytest.mark.parametrize(
    (
        "addon_version",
        "update_available",
        "update_calls",
        "backup_calls",
        "update_addon_side_effect",
        "create_backup_side_effect",
    ),
    [
        ("1.0.0", True, 1, 1, None, None),
        ("1.0.0", False, 0, 0, None, None),
        ("1.0.0", True, 1, 1, HassioAPIError("Boom"), None),
        ("1.0.0", True, 0, 1, None, HassioAPIError("Boom")),
    ],
)
async def test_update_addon(
    hass: HomeAssistant,
    addon_installed: AsyncMock,
    addon_running: AsyncMock,
    addon_info: AsyncMock,
    install_addon: AsyncMock,
    start_addon: AsyncMock,
    create_backup: AsyncMock,
    update_addon: AsyncMock,
    matter_client: MagicMock,
    addon_version: str,
    update_available: bool,
    update_calls: int,
    backup_calls: int,
    update_addon_side_effect: Exception | None,
    create_backup_side_effect: Exception | None,
):
    """Test update the Matter add-on during entry setup."""
    addon_info.return_value["version"] = addon_version
    addon_info.return_value["update_available"] = update_available
    create_backup.side_effect = create_backup_side_effect
    update_addon.side_effect = update_addon_side_effect
    matter_client.connect.side_effect = InvalidServerVersion("Invalid version")
    entry = MockConfigEntry(
        domain=DOMAIN,
        title="Matter",
        data={
            "url": "ws://host1:5581/ws",
            "use_addon": True,
        },
    )
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.SETUP_RETRY
    assert create_backup.call_count == backup_calls
    assert update_addon.call_count == update_calls


async def test_issue_registry_invalid_version(
    hass: HomeAssistant,
    matter_client: MagicMock,
) -> None:
    """Test issue registry for invalid version."""
    original_connect_side_effect = matter_client.connect.side_effect
    matter_client.connect.side_effect = InvalidServerVersion("Invalid version")
    entry = MockConfigEntry(
        domain=DOMAIN,
        title="Matter",
        data={
            "url": "ws://host1:5581/ws",
            "use_addon": False,
        },
    )
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    issue_reg = ir.async_get(hass)
    entry_state = entry.state
    assert entry_state is ConfigEntryState.SETUP_RETRY
    assert issue_reg.async_get_issue(DOMAIN, "invalid_server_version")

    matter_client.connect.side_effect = original_connect_side_effect

    await hass.config_entries.async_reload(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.LOADED
    assert not issue_reg.async_get_issue(DOMAIN, "invalid_server_version")


@pytest.mark.parametrize(
    ("stop_addon_side_effect", "entry_state"),
    [
        (None, ConfigEntryState.NOT_LOADED),
        (HassioAPIError("Boom"), ConfigEntryState.LOADED),
    ],
)
async def test_stop_addon(
    hass,
    matter_client: MagicMock,
    addon_installed: AsyncMock,
    addon_running: AsyncMock,
    addon_info: AsyncMock,
    stop_addon: AsyncMock,
    stop_addon_side_effect: Exception | None,
    entry_state: ConfigEntryState,
):
    """Test stop the Matter add-on on entry unload if entry is disabled."""
    stop_addon.side_effect = stop_addon_side_effect
    entry = MockConfigEntry(
        domain=DOMAIN,
        title="Matter",
        data={
            "url": "ws://host1:5581/ws",
            "use_addon": True,
        },
    )
    entry.add_to_hass(hass)

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.LOADED
    assert addon_info.call_count == 1
    addon_info.reset_mock()

    await hass.config_entries.async_set_disabled_by(
        entry.entry_id, ConfigEntryDisabler.USER
    )
    await hass.async_block_till_done()

    assert entry.state == entry_state
    assert stop_addon.call_count == 1
    assert stop_addon.call_args == call(hass, "core_matter_server")


async def test_remove_entry(
    hass: HomeAssistant,
    addon_installed: AsyncMock,
    stop_addon: AsyncMock,
    create_backup: AsyncMock,
    uninstall_addon: AsyncMock,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test remove the config entry."""
    # test successful remove without created add-on
    entry = MockConfigEntry(
        domain=DOMAIN,
        title="Matter",
        data={"integration_created_addon": False},
    )
    entry.add_to_hass(hass)
    assert entry.state is ConfigEntryState.NOT_LOADED
    assert len(hass.config_entries.async_entries(DOMAIN)) == 1

    await hass.config_entries.async_remove(entry.entry_id)

    assert entry.state is ConfigEntryState.NOT_LOADED
    assert len(hass.config_entries.async_entries(DOMAIN)) == 0

    # test successful remove with created add-on
    entry = MockConfigEntry(
        domain=DOMAIN,
        title="Matter",
        data={"integration_created_addon": True},
    )
    entry.add_to_hass(hass)
    assert len(hass.config_entries.async_entries(DOMAIN)) == 1

    await hass.config_entries.async_remove(entry.entry_id)

    assert stop_addon.call_count == 1
    assert stop_addon.call_args == call(hass, "core_matter_server")
    assert create_backup.call_count == 1
    assert create_backup.call_args == call(
        hass,
        {"name": "addon_core_matter_server_1.0.0", "addons": ["core_matter_server"]},
        partial=True,
    )
    assert uninstall_addon.call_count == 1
    assert uninstall_addon.call_args == call(hass, "core_matter_server")
    assert entry.state is ConfigEntryState.NOT_LOADED
    assert len(hass.config_entries.async_entries(DOMAIN)) == 0
    stop_addon.reset_mock()
    create_backup.reset_mock()
    uninstall_addon.reset_mock()

    # test add-on stop failure
    entry.add_to_hass(hass)
    assert len(hass.config_entries.async_entries(DOMAIN)) == 1
    stop_addon.side_effect = HassioAPIError()

    await hass.config_entries.async_remove(entry.entry_id)

    assert stop_addon.call_count == 1
    assert stop_addon.call_args == call(hass, "core_matter_server")
    assert create_backup.call_count == 0
    assert uninstall_addon.call_count == 0
    assert entry.state is ConfigEntryState.NOT_LOADED
    assert len(hass.config_entries.async_entries(DOMAIN)) == 0
    assert "Failed to stop the Matter Server add-on" in caplog.text
    stop_addon.side_effect = None
    stop_addon.reset_mock()
    create_backup.reset_mock()
    uninstall_addon.reset_mock()

    # test create backup failure
    entry.add_to_hass(hass)
    assert len(hass.config_entries.async_entries(DOMAIN)) == 1
    create_backup.side_effect = HassioAPIError()

    await hass.config_entries.async_remove(entry.entry_id)

    assert stop_addon.call_count == 1
    assert stop_addon.call_args == call(hass, "core_matter_server")
    assert create_backup.call_count == 1
    assert create_backup.call_args == call(
        hass,
        {"name": "addon_core_matter_server_1.0.0", "addons": ["core_matter_server"]},
        partial=True,
    )
    assert uninstall_addon.call_count == 0
    assert entry.state is ConfigEntryState.NOT_LOADED
    assert len(hass.config_entries.async_entries(DOMAIN)) == 0
    assert "Failed to create a backup of the Matter Server add-on" in caplog.text
    create_backup.side_effect = None
    stop_addon.reset_mock()
    create_backup.reset_mock()
    uninstall_addon.reset_mock()

    # test add-on uninstall failure
    entry.add_to_hass(hass)
    assert len(hass.config_entries.async_entries(DOMAIN)) == 1
    uninstall_addon.side_effect = HassioAPIError()

    await hass.config_entries.async_remove(entry.entry_id)

    assert stop_addon.call_count == 1
    assert stop_addon.call_args == call(hass, "core_matter_server")
    assert create_backup.call_count == 1
    assert create_backup.call_args == call(
        hass,
        {"name": "addon_core_matter_server_1.0.0", "addons": ["core_matter_server"]},
        partial=True,
    )
    assert uninstall_addon.call_count == 1
    assert uninstall_addon.call_args == call(hass, "core_matter_server")
    assert entry.state is ConfigEntryState.NOT_LOADED
    assert len(hass.config_entries.async_entries(DOMAIN)) == 0
    assert "Failed to uninstall the Matter Server add-on" in caplog.text


async def test_remove_config_entry_device(
    hass: HomeAssistant,
    matter_client: MagicMock,
    hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]],
) -> None:
    """Test that a device can be removed ok."""
    assert await async_setup_component(hass, "config", {})
    await setup_integration_with_node_fixture(hass, "device_diagnostics", matter_client)
    await hass.async_block_till_done()

    config_entry = hass.config_entries.async_entries(DOMAIN)[0]
    device_registry = dr.async_get(hass)
    device_entry = dr.async_entries_for_config_entry(
        device_registry, config_entry.entry_id
    )[0]
    entity_registry = er.async_get(hass)
    entity_id = "light.m5stamp_lighting_app"

    assert device_entry
    assert entity_registry.async_get(entity_id)
    assert hass.states.get(entity_id)

    client = await hass_ws_client(hass)
    await client.send_json(
        {
            "id": 5,
            "type": "config/device_registry/remove_config_entry",
            "config_entry_id": config_entry.entry_id,
            "device_id": device_entry.id,
        }
    )
    response = await client.receive_json()
    assert response["success"]
    await hass.async_block_till_done()

    assert not device_registry.async_get(device_entry.id)
    assert not entity_registry.async_get(entity_id)
    assert not hass.states.get(entity_id)


async def test_remove_config_entry_device_no_node(
    hass: HomeAssistant,
    matter_client: MagicMock,
    integration: MockConfigEntry,
    hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]],
) -> None:
    """Test that a device can be removed ok without an existing node."""
    assert await async_setup_component(hass, "config", {})
    config_entry = integration
    device_registry = dr.async_get(hass)
    device_entry = device_registry.async_get_or_create(
        config_entry_id=config_entry.entry_id,
        identifiers={
            (DOMAIN, "deviceid_00000000000004D2-0000000000000005-MatterNodeDevice")
        },
    )

    client = await hass_ws_client(hass)
    await client.send_json(
        {
            "id": 5,
            "type": "config/device_registry/remove_config_entry",
            "config_entry_id": config_entry.entry_id,
            "device_id": device_entry.id,
        }
    )
    response = await client.receive_json()
    assert response["success"]
    await hass.async_block_till_done()

    assert not device_registry.async_get(device_entry.id)