"""Test the Open Thread Border Router integration."""

import asyncio
from typing import Any
from unittest.mock import ANY, AsyncMock, MagicMock, patch

import aiohttp
import pytest
import python_otbr_api
from zeroconf.asyncio import AsyncServiceInfo

from homeassistant.components import otbr, thread
from homeassistant.components.thread import discovery
from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_USER
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component

from . import (
    BASE_URL,
    CONFIG_ENTRY_DATA_MULTIPAN,
    DATASET_CH15,
    DATASET_CH16,
    DATASET_INSECURE_NW_KEY,
    DATASET_INSECURE_PASSPHRASE,
    ROUTER_DISCOVERY_HASS,
    TEST_BORDER_AGENT_EXTENDED_ADDRESS,
    TEST_BORDER_AGENT_ID,
    TEST_COPROCESSOR_VERSION,
)

from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker

DATASET_NO_CHANNEL = bytes.fromhex(
    "0E08000000000001000035060004001FFFE00208F642646DA209B1C00708FDF57B5A"
    "0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102"
    "25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8"
)


@pytest.fixture(name="enable_mocks", autouse=True)
def enable_mocks_fixture(
    get_active_dataset_tlvs: AsyncMock,
    get_border_agent_id: AsyncMock,
    get_extended_address: AsyncMock,
    get_coprocessor_version: AsyncMock,
) -> None:
    """Enable API mocks."""


@pytest.mark.usefixtures("supervisor_client")
async def test_import_dataset(
    hass: HomeAssistant,
    mock_async_zeroconf: MagicMock,
    issue_registry: ir.IssueRegistry,
) -> None:
    """Test the active dataset is imported at setup."""
    add_service_listener_called = asyncio.Event()

    async def mock_add_service_listener(type_: str, listener: Any):
        add_service_listener_called.set()

    mock_async_zeroconf.async_add_service_listener = AsyncMock(
        side_effect=mock_add_service_listener
    )
    mock_async_zeroconf.async_remove_service_listener = AsyncMock()
    mock_async_zeroconf.async_get_service_info = AsyncMock()

    assert await thread.async_get_preferred_dataset(hass) is None

    config_entry = MockConfigEntry(
        data=CONFIG_ENTRY_DATA_MULTIPAN,
        domain=otbr.DOMAIN,
        options={},
        title="My OTBR",
        unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
    )
    config_entry.add_to_hass(hass)

    with (
        patch(
            "homeassistant.components.thread.dataset_store.BORDER_AGENT_DISCOVERY_TIMEOUT",
            0.1,
        ),
    ):
        assert await hass.config_entries.async_setup(config_entry.entry_id)

        # Wait for Thread router discovery to start
        await add_service_listener_called.wait()
        mock_async_zeroconf.async_add_service_listener.assert_called_once_with(
            "_meshcop._udp.local.", ANY
        )

        # Discover a service matching our router
        listener: discovery.ThreadRouterDiscovery.ThreadServiceListener = (
            mock_async_zeroconf.async_add_service_listener.mock_calls[0][1][1]
        )
        mock_async_zeroconf.async_get_service_info.return_value = AsyncServiceInfo(
            **ROUTER_DISCOVERY_HASS
        )
        listener.add_service(
            None, ROUTER_DISCOVERY_HASS["type_"], ROUTER_DISCOVERY_HASS["name"]
        )

        # Wait for discovery of other routers to time out
        await hass.async_block_till_done()

    dataset_store = await thread.dataset_store.async_get_store(hass)
    assert (
        list(dataset_store.datasets.values())[0].preferred_border_agent_id
        == TEST_BORDER_AGENT_ID.hex()
    )
    assert (
        list(dataset_store.datasets.values())[0].preferred_extended_address
        == TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex()
    )
    assert await thread.async_get_preferred_dataset(hass) == DATASET_CH16.hex()
    assert not issue_registry.async_get_issue(
        domain=otbr.DOMAIN, issue_id=f"insecure_thread_network_{config_entry.entry_id}"
    )
    assert not issue_registry.async_get_issue(
        domain=otbr.DOMAIN,
        issue_id=f"otbr_zha_channel_collision_{config_entry.entry_id}",
    )


async def test_import_share_radio_channel_collision(
    hass: HomeAssistant,
    multiprotocol_addon_manager_mock,
    issue_registry: ir.IssueRegistry,
) -> None:
    """Test the active dataset is imported at setup.

    This imports a dataset with different channel than ZHA when ZHA and OTBR share
    the radio.
    """
    multiprotocol_addon_manager_mock.async_get_channel.return_value = 15

    config_entry = MockConfigEntry(
        data=CONFIG_ENTRY_DATA_MULTIPAN,
        domain=otbr.DOMAIN,
        options={},
        title="My OTBR",
        unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
    )
    config_entry.add_to_hass(hass)
    with (
        patch(
            "homeassistant.components.thread.dataset_store.DatasetStore.async_add"
        ) as mock_add,
    ):
        assert await hass.config_entries.async_setup(config_entry.entry_id)

    mock_add.assert_called_once_with(
        otbr.DOMAIN,
        DATASET_CH16.hex(),
        TEST_BORDER_AGENT_ID.hex(),
        TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
    )
    assert issue_registry.async_get_issue(
        domain=otbr.DOMAIN,
        issue_id=f"otbr_zha_channel_collision_{config_entry.entry_id}",
    )


@pytest.mark.parametrize("dataset", [DATASET_CH15, DATASET_NO_CHANNEL])
async def test_import_share_radio_no_channel_collision(
    hass: HomeAssistant,
    multiprotocol_addon_manager_mock,
    dataset: bytes,
    issue_registry: ir.IssueRegistry,
) -> None:
    """Test the active dataset is imported at setup.

    This imports a dataset when ZHA and OTBR share the radio.
    """
    multiprotocol_addon_manager_mock.async_get_channel.return_value = 15

    config_entry = MockConfigEntry(
        data=CONFIG_ENTRY_DATA_MULTIPAN,
        domain=otbr.DOMAIN,
        options={},
        title="My OTBR",
        unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
    )
    config_entry.add_to_hass(hass)
    with (
        patch(
            "homeassistant.components.thread.dataset_store.DatasetStore.async_add"
        ) as mock_add,
    ):
        assert await hass.config_entries.async_setup(config_entry.entry_id)

    mock_add.assert_called_once_with(
        otbr.DOMAIN,
        dataset.hex(),
        TEST_BORDER_AGENT_ID.hex(),
        TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
    )
    assert not issue_registry.async_get_issue(
        domain=otbr.DOMAIN,
        issue_id=f"otbr_zha_channel_collision_{config_entry.entry_id}",
    )


@pytest.mark.usefixtures("supervisor_client")
@pytest.mark.parametrize("enable_compute_pskc", [True])
@pytest.mark.parametrize(
    "dataset", [DATASET_INSECURE_NW_KEY, DATASET_INSECURE_PASSPHRASE]
)
async def test_import_insecure_dataset(
    hass: HomeAssistant, dataset: bytes, issue_registry: ir.IssueRegistry
) -> None:
    """Test the active dataset is imported at setup.

    This imports a dataset with insecure settings.
    """
    config_entry = MockConfigEntry(
        data=CONFIG_ENTRY_DATA_MULTIPAN,
        domain=otbr.DOMAIN,
        options={},
        title="My OTBR",
        unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
    )
    config_entry.add_to_hass(hass)
    with (
        patch(
            "homeassistant.components.thread.dataset_store.DatasetStore.async_add"
        ) as mock_add,
    ):
        assert await hass.config_entries.async_setup(config_entry.entry_id)

    mock_add.assert_called_once_with(
        otbr.DOMAIN,
        dataset.hex(),
        TEST_BORDER_AGENT_ID.hex(),
        TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
    )
    assert issue_registry.async_get_issue(
        domain=otbr.DOMAIN, issue_id=f"insecure_thread_network_{config_entry.entry_id}"
    )


@pytest.mark.parametrize(
    "error",
    [
        TimeoutError,
        python_otbr_api.OTBRError,
        aiohttp.ClientError,
    ],
)
async def test_config_entry_not_ready(
    hass: HomeAssistant, get_active_dataset_tlvs: AsyncMock, error
) -> None:
    """Test raising ConfigEntryNotReady ."""

    config_entry = MockConfigEntry(
        data=CONFIG_ENTRY_DATA_MULTIPAN,
        domain=otbr.DOMAIN,
        options={},
        title="My OTBR",
        unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
    )
    config_entry.add_to_hass(hass)
    get_active_dataset_tlvs.side_effect = error
    assert not await hass.config_entries.async_setup(config_entry.entry_id)


async def test_border_agent_id_not_supported(
    hass: HomeAssistant, get_border_agent_id: AsyncMock
) -> None:
    """Test border router does not support border agent ID."""

    config_entry = MockConfigEntry(
        data=CONFIG_ENTRY_DATA_MULTIPAN,
        domain=otbr.DOMAIN,
        options={},
        title="My OTBR",
        unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
    )
    config_entry.add_to_hass(hass)
    get_border_agent_id.side_effect = python_otbr_api.GetBorderAgentIdNotSupportedError
    assert not await hass.config_entries.async_setup(config_entry.entry_id)


async def test_config_entry_update(hass: HomeAssistant) -> None:
    """Test update config entry settings."""
    config_entry = MockConfigEntry(
        data=CONFIG_ENTRY_DATA_MULTIPAN,
        domain=otbr.DOMAIN,
        options={},
        title="My OTBR",
        unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
    )
    config_entry.add_to_hass(hass)
    mock_api = MagicMock()
    mock_api.get_active_dataset_tlvs = AsyncMock(return_value=None)
    mock_api.get_border_agent_id = AsyncMock(return_value=TEST_BORDER_AGENT_ID)
    mock_api.get_extended_address = AsyncMock(
        return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS
    )
    mock_api.get_coprocessor_version = AsyncMock(return_value=TEST_COPROCESSOR_VERSION)
    with patch("python_otbr_api.OTBR", return_value=mock_api) as mock_otrb_api:
        assert await hass.config_entries.async_setup(config_entry.entry_id)

    mock_otrb_api.assert_called_once_with(CONFIG_ENTRY_DATA_MULTIPAN["url"], ANY, ANY)

    new_config_entry_data = {"url": "http://core-silabs-multiprotocol:8082"}
    assert CONFIG_ENTRY_DATA_MULTIPAN["url"] != new_config_entry_data["url"]
    with patch("python_otbr_api.OTBR", return_value=mock_api) as mock_otrb_api:
        hass.config_entries.async_update_entry(config_entry, data=new_config_entry_data)
        await hass.async_block_till_done()

    mock_otrb_api.assert_called_once_with(new_config_entry_data["url"], ANY, ANY)


@pytest.mark.usefixtures("supervisor_client")
async def test_remove_entry(
    hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry_multipan
) -> None:
    """Test async_get_active_dataset_tlvs after removing the config entry."""

    aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text="0E")

    config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0]
    await hass.config_entries.async_remove(config_entry.entry_id)


@pytest.mark.parametrize(
    ("source", "unique_id", "updated_unique_id"),
    [
        (SOURCE_HASSIO, None, None),
        (SOURCE_HASSIO, "abcd", "abcd"),
        (SOURCE_USER, None, TEST_BORDER_AGENT_ID.hex()),
        (SOURCE_USER, "abcd", TEST_BORDER_AGENT_ID.hex()),
    ],
)
async def test_update_unique_id(
    hass: HomeAssistant,
    aioclient_mock: AiohttpClientMocker,
    source: str,
    unique_id: str | None,
    updated_unique_id: str | None,
) -> None:
    """Test we update the unique id if extended address has changed."""

    config_entry = MockConfigEntry(
        data=CONFIG_ENTRY_DATA_MULTIPAN,
        domain=otbr.DOMAIN,
        options={},
        source=source,
        title="Open Thread Border Router",
        unique_id=unique_id,
    )
    config_entry.add_to_hass(hass)
    assert await async_setup_component(hass, otbr.DOMAIN, {})
    config_entry = hass.config_entries.async_get_entry(config_entry.entry_id)
    assert config_entry.unique_id == updated_unique_id
