"""Test Zeroconf component setup process."""
from typing import Any
from unittest.mock import call, patch

import pytest
from zeroconf import (
    BadTypeInNameException,
    InterfaceChoice,
    IPVersion,
    ServiceStateChange,
)
from zeroconf.asyncio import AsyncServiceInfo

from homeassistant.components import zeroconf
from homeassistant.const import (
    EVENT_COMPONENT_LOADED,
    EVENT_HOMEASSISTANT_START,
    EVENT_HOMEASSISTANT_STARTED,
    EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant
from homeassistant.generated import zeroconf as zc_gen
from homeassistant.setup import ATTR_COMPONENT, async_setup_component

NON_UTF8_VALUE = b"ABCDEF\x8a"
NON_ASCII_KEY = b"non-ascii-key\x8a"
PROPERTIES = {
    b"macaddress": b"ABCDEF012345",
    b"non-utf8-value": NON_UTF8_VALUE,
    NON_ASCII_KEY: None,
}

HOMEKIT_STATUS_UNPAIRED = b"1"
HOMEKIT_STATUS_PAIRED = b"0"


def service_update_mock(ipv6, zeroconf, services, handlers, *, limit_service=None):
    """Call service update handler."""
    for service in services:
        if limit_service is not None and service != limit_service:
            continue
        handlers[0](zeroconf, service, f"_name.{service}", ServiceStateChange.Added)


def get_service_info_mock(
    service_type: str, name: str, *args: Any, **kwargs: Any
) -> AsyncServiceInfo:
    """Return service info for get_service_info."""
    return AsyncServiceInfo(
        service_type,
        name,
        addresses=[b"\n\x00\x00\x14"],
        port=80,
        weight=0,
        priority=0,
        server="name.local.",
        properties=PROPERTIES,
    )


def get_service_info_mock_without_an_address(
    service_type: str, name: str
) -> AsyncServiceInfo:
    """Return service info for get_service_info without any addresses."""
    return AsyncServiceInfo(
        service_type,
        name,
        addresses=[],
        port=80,
        weight=0,
        priority=0,
        server="name.local.",
        properties=PROPERTIES,
    )


def get_homekit_info_mock(model, pairing_status):
    """Return homekit info for get_service_info for an homekit device."""

    def mock_homekit_info(service_type, name):
        return AsyncServiceInfo(
            service_type,
            name,
            addresses=[b"\n\x00\x00\x14"],
            port=80,
            weight=0,
            priority=0,
            server="name.local.",
            properties={b"md": model.encode(), b"sf": pairing_status},
        )

    return mock_homekit_info


def get_zeroconf_info_mock(macaddress):
    """Return info for get_service_info for an zeroconf device."""

    def mock_zc_info(service_type, name):
        return AsyncServiceInfo(
            service_type,
            name,
            addresses=[b"\n\x00\x00\x14"],
            port=80,
            weight=0,
            priority=0,
            server="name.local.",
            properties={b"macaddress": macaddress.encode()},
        )

    return mock_zc_info


def get_zeroconf_info_mock_manufacturer(manufacturer):
    """Return info for get_service_info for an zeroconf device."""

    def mock_zc_info(service_type, name):
        return AsyncServiceInfo(
            service_type,
            name,
            addresses=[b"\n\x00\x00\x14"],
            port=80,
            weight=0,
            priority=0,
            server="name.local.",
            properties={b"manufacturer": manufacturer.encode()},
        )

    return mock_zc_info


def get_zeroconf_info_mock_model(model):
    """Return info for get_service_info for an zeroconf device."""

    def mock_zc_info(service_type, name):
        return AsyncServiceInfo(
            service_type,
            name,
            addresses=[b"\n\x00\x00\x14"],
            port=80,
            weight=0,
            priority=0,
            server="name.local.",
            properties={b"model": model.encode()},
        )

    return mock_zc_info


async def test_setup(hass: HomeAssistant, mock_async_zeroconf: None) -> None:
    """Test configured options for a device are loaded via config entry."""
    mock_zc = {
        "_http._tcp.local.": [
            {
                "domain": "shelly",
                "name": "shelly*",
                "properties": {"macaddress": "ffaadd*"},
            }
        ],
        "_Volumio._tcp.local.": [{"domain": "volumio"}],
    }
    with patch.dict(
        zc_gen.ZEROCONF,
        mock_zc,
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_service_info_mock,
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    expected_flow_calls = 0
    for matching_components in mock_zc.values():
        domains = set()
        for component in matching_components:
            if len(component) == 1:
                domains.add(component["domain"])
        expected_flow_calls += len(domains)
    assert len(mock_config_flow.mock_calls) == expected_flow_calls

    # Test instance is set.
    assert "zeroconf" in hass.data
    assert (
        await hass.components.zeroconf.async_get_async_instance() is mock_async_zeroconf
    )


async def test_setup_with_overly_long_url_and_name(
    hass: HomeAssistant, mock_async_zeroconf: None, caplog: pytest.LogCaptureFixture
) -> None:
    """Test we still setup with long urls and names."""
    with patch.object(hass.config_entries.flow, "async_init"), patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ), patch(
        "homeassistant.components.zeroconf.get_url",
        return_value=(
            "https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over"
            "/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup"
            "/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a"
            "/bit/longer/than/the/maximum/length/that/we/allow/for/a/value"
        ),
    ), patch.object(
        hass.config,
        "location_name",
        (
            "\u00dcBER \u00dcber German Umlaut long string long string long string long"
            " string long string long string long string long string long string long"
            " string long string long string long string long string long string long"
            " string long string long string long string long string long string long"
            " string long string long string long string long string long string long"
            " string long string long string long string long string long string long"
            " string long string long string long string long string long string long"
            " string long string long string long string long string long string long"
            " string long string long string long string long string"
        ),
    ), patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo.request",
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
        await hass.async_block_till_done()

    assert "https://this.url.is.way.too.long" in caplog.text
    assert "German Umlaut" in caplog.text


async def test_setup_with_defaults(
    hass: HomeAssistant, mock_zeroconf: None, mock_async_zeroconf: None
) -> None:
    """Test default interface config."""
    with patch.object(hass.config_entries.flow, "async_init"), patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ), patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_service_info_mock,
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    mock_zeroconf.assert_called_with(
        interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only
    )


async def test_zeroconf_match_macaddress(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test configured options for a device are loaded via config entry."""

    def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
        """Call service update handler."""
        handlers[0](
            zeroconf,
            "_http._tcp.local.",
            "Shelly108._http._tcp.local.",
            ServiceStateChange.Added,
        )

    with patch.dict(
        zc_gen.ZEROCONF,
        {
            "_http._tcp.local.": [
                {
                    "domain": "shelly",
                    "name": "shelly*",
                    "properties": {"macaddress": "ffaadd*"},
                }
            ]
        },
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 1
    assert mock_config_flow.mock_calls[0][1][0] == "shelly"
    assert mock_config_flow.mock_calls[0][2]["context"] == {"source": "zeroconf"}


async def test_zeroconf_match_manufacturer(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test configured options for a device are loaded via config entry."""

    def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
        """Call service update handler."""
        handlers[0](
            zeroconf,
            "_airplay._tcp.local.",
            "s1000._airplay._tcp.local.",
            ServiceStateChange.Added,
        )

    with patch.dict(
        zc_gen.ZEROCONF,
        {
            "_airplay._tcp.local.": [
                {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}}
            ]
        },
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 1
    assert mock_config_flow.mock_calls[0][1][0] == "samsungtv"


async def test_zeroconf_match_model(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test matching a specific model in zeroconf."""

    def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
        """Call service update handler."""
        handlers[0](
            zeroconf,
            "_airplay._tcp.local.",
            "s1000._airplay._tcp.local.",
            ServiceStateChange.Added,
        )

    with patch.dict(
        zc_gen.ZEROCONF,
        {
            "_airplay._tcp.local.": [
                {"domain": "appletv", "properties": {"model": "appletv*"}}
            ]
        },
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_zeroconf_info_mock_model("appletv"),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 1
    assert mock_config_flow.mock_calls[0][1][0] == "appletv"


async def test_zeroconf_match_manufacturer_not_present(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test matchers reject when a property is missing."""

    def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
        """Call service update handler."""
        handlers[0](
            zeroconf,
            "_airplay._tcp.local.",
            "s1000._airplay._tcp.local.",
            ServiceStateChange.Added,
        )

    with patch.dict(
        zc_gen.ZEROCONF,
        {
            "_airplay._tcp.local.": [
                {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}}
            ]
        },
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_zeroconf_info_mock("aabbccddeeff"),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 0


async def test_zeroconf_no_match(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test configured options for a device are loaded via config entry."""

    def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
        """Call service update handler."""
        handlers[0](
            zeroconf,
            "_http._tcp.local.",
            "somethingelse._http._tcp.local.",
            ServiceStateChange.Added,
        )

    with patch.dict(
        zc_gen.ZEROCONF,
        {"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]},
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 0


async def test_zeroconf_no_match_manufacturer(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test configured options for a device are loaded via config entry."""

    def http_only_service_update_mock(ipv6, zeroconf, services, handlers):
        """Call service update handler."""
        handlers[0](
            zeroconf,
            "_airplay._tcp.local.",
            "s1000._airplay._tcp.local.",
            ServiceStateChange.Added,
        )

    with patch.dict(
        zc_gen.ZEROCONF,
        {
            "_airplay._tcp.local.": [
                {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}}
            ]
        },
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 0


async def test_homekit_match_partial_space(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test configured options for a device are loaded via config entry."""
    with patch.dict(
        zc_gen.ZEROCONF,
        {"_hap._tcp.local.": [{"domain": "homekit_controller"}]},
        clear=True,
    ), patch.dict(
        zc_gen.HOMEKIT,
        {"LIFX": {"domain": "lifx", "always_discover": True}},
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf,
        "HaAsyncServiceBrowser",
        side_effect=lambda *args, **kwargs: service_update_mock(
            *args, **kwargs, limit_service="_hap._tcp.local."
        ),
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    # One for HKC, and one for LIFX since lifx is local polling
    assert len(mock_config_flow.mock_calls) == 2
    assert mock_config_flow.mock_calls[0][1][0] == "lifx"
    assert mock_config_flow.mock_calls[1][2]["context"] == {
        "source": "zeroconf",
        "alternative_domain": "lifx",
    }


async def test_device_with_invalid_name(
    hass: HomeAssistant, mock_async_zeroconf: None, caplog: pytest.LogCaptureFixture
) -> None:
    """Test we ignore devices with an invalid name."""
    with patch.dict(
        zc_gen.ZEROCONF,
        {"_hap._tcp.local.": [{"domain": "homekit_controller"}]},
        clear=True,
    ), patch.dict(
        zc_gen.HOMEKIT,
        {"LIFX": {"domain": "lifx", "always_discover": True}},
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf,
        "HaAsyncServiceBrowser",
        side_effect=lambda *args, **kwargs: service_update_mock(
            *args, **kwargs, limit_service="_hap._tcp.local."
        ),
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=BadTypeInNameException,
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 0
    assert "Bad name in zeroconf record" in caplog.text


async def test_homekit_match_partial_dash(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test configured options for a device are loaded via config entry."""
    with patch.dict(
        zc_gen.ZEROCONF,
        {"_hap._udp.local.": [{"domain": "homekit_controller"}]},
        clear=True,
    ), patch.dict(
        zc_gen.HOMEKIT,
        {"Smart Bridge": {"domain": "lutron_caseta", "always_discover": False}},
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf,
        "HaAsyncServiceBrowser",
        side_effect=lambda *args, **kwargs: service_update_mock(
            *args, **kwargs, limit_service="_hap._udp.local."
        ),
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_homekit_info_mock("Smart Bridge-001", HOMEKIT_STATUS_UNPAIRED),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 1
    assert mock_config_flow.mock_calls[0][1][0] == "lutron_caseta"


async def test_homekit_match_partial_fnmatch(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test matching homekit devices with fnmatch."""
    with patch.dict(
        zc_gen.ZEROCONF,
        {"_hap._tcp.local.": [{"domain": "homekit_controller"}]},
        clear=True,
    ), patch.dict(
        zc_gen.HOMEKIT,
        {"YLDP*": {"domain": "yeelight", "always_discover": False}},
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf,
        "HaAsyncServiceBrowser",
        side_effect=lambda *args, **kwargs: service_update_mock(
            *args, **kwargs, limit_service="_hap._tcp.local."
        ),
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_homekit_info_mock("YLDP13YL", HOMEKIT_STATUS_UNPAIRED),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 1
    assert mock_config_flow.mock_calls[0][1][0] == "yeelight"


async def test_homekit_match_full(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test configured options for a device are loaded via config entry."""
    with patch.dict(
        zc_gen.ZEROCONF,
        {"_hap._udp.local.": [{"domain": "homekit_controller"}]},
        clear=True,
    ), patch.dict(
        zc_gen.HOMEKIT,
        {"BSB002": {"domain": "hue", "always_discover": False}},
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf,
        "HaAsyncServiceBrowser",
        side_effect=lambda *args, **kwargs: service_update_mock(
            *args, **kwargs, limit_service="_hap._udp.local."
        ),
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 1
    assert mock_config_flow.mock_calls[0][1][0] == "hue"


async def test_homekit_already_paired(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test that an already paired device is sent to homekit_controller."""
    with patch.dict(
        zc_gen.ZEROCONF,
        {"_hap._tcp.local.": [{"domain": "homekit_controller"}]},
        clear=True,
    ), patch.dict(
        zc_gen.HOMEKIT,
        {
            "AC02": {"domain": "tado", "always_discover": True},
            "tado": {"domain": "tado", "always_discover": True},
        },
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf,
        "HaAsyncServiceBrowser",
        side_effect=lambda *args, **kwargs: service_update_mock(
            *args, **kwargs, limit_service="_hap._tcp.local."
        ),
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 2
    assert mock_config_flow.mock_calls[0][1][0] == "tado"
    assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller"


async def test_homekit_invalid_paring_status(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test that missing paring data is not sent to homekit_controller."""
    with patch.dict(
        zc_gen.ZEROCONF,
        {"_hap._tcp.local.": [{"domain": "homekit_controller"}]},
        clear=True,
    ), patch.dict(
        zc_gen.HOMEKIT,
        {"Smart Bridge": {"domain": "lutron_caseta", "always_discover": False}},
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf,
        "HaAsyncServiceBrowser",
        side_effect=lambda *args, **kwargs: service_update_mock(
            *args, **kwargs, limit_service="_hap._tcp.local."
        ),
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_homekit_info_mock("Smart Bridge", b"invalid"),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 1
    assert mock_config_flow.mock_calls[0][1][0] == "lutron_caseta"


async def test_homekit_not_paired(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test that an not paired device is sent to homekit_controller."""
    with patch.dict(
        zc_gen.ZEROCONF,
        {"_hap._tcp.local.": [{"domain": "homekit_controller"}]},
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_homekit_info_mock(
            "this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED
        ),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 1
    assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller"


async def test_homekit_controller_still_discovered_unpaired_for_cloud(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test discovery is still passed to homekit controller when unpaired.

    When unpaired and discovered by cloud integration.

    Since we prefer local control, if the integration that is being discovered
    is cloud AND the homekit device is unpaired we still want to discovery it
    """
    with patch.dict(
        zc_gen.ZEROCONF,
        {"_hap._udp.local.": [{"domain": "homekit_controller"}]},
        clear=True,
    ), patch.dict(
        zc_gen.HOMEKIT,
        {"Rachio": {"domain": "rachio", "always_discover": True}},
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf,
        "HaAsyncServiceBrowser",
        side_effect=lambda *args, **kwargs: service_update_mock(
            *args, **kwargs, limit_service="_hap._udp.local."
        ),
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_homekit_info_mock("Rachio-xyz", HOMEKIT_STATUS_UNPAIRED),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 2
    assert mock_config_flow.mock_calls[0][1][0] == "rachio"
    assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller"


async def test_homekit_controller_still_discovered_unpaired_for_polling(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test discovery is still passed to homekit controller when unpaired.

    When unpaired and discovered by polling integration.

    Since we prefer local push, if the integration that is being discovered
    is polling AND the homekit device is unpaired we still want to discovery it
    """
    with patch.dict(
        zc_gen.ZEROCONF,
        {"_hap._udp.local.": [{"domain": "homekit_controller"}]},
        clear=True,
    ), patch.dict(
        zc_gen.HOMEKIT,
        {"iSmartGate": {"domain": "gogogate2", "always_discover": True}},
        clear=True,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_config_flow, patch.object(
        zeroconf,
        "HaAsyncServiceBrowser",
        side_effect=lambda *args, **kwargs: service_update_mock(
            *args, **kwargs, limit_service="_hap._udp.local."
        ),
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_homekit_info_mock("iSmartGate", HOMEKIT_STATUS_UNPAIRED),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_config_flow.mock_calls) == 2
    assert mock_config_flow.mock_calls[0][1][0] == "gogogate2"
    assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller"


async def test_info_from_service_non_utf8(hass: HomeAssistant) -> None:
    """Test info_from_service handles non UTF-8 property keys and values correctly."""
    service_type = "_test._tcp.local."
    info = zeroconf.info_from_service(
        get_service_info_mock(service_type, f"test.{service_type}")
    )
    assert NON_ASCII_KEY.decode("ascii", "replace") in info.properties
    assert "non-utf8-value" in info.properties
    assert info.properties["non-utf8-value"] == NON_UTF8_VALUE.decode(
        "utf-8", "replace"
    )


async def test_info_from_service_with_addresses(hass: HomeAssistant) -> None:
    """Test info_from_service does not throw when there are no addresses."""
    service_type = "_test._tcp.local."
    info = zeroconf.info_from_service(
        get_service_info_mock_without_an_address(service_type, f"test.{service_type}")
    )
    assert info is None


async def test_info_from_service_with_link_local_address_first(
    hass: HomeAssistant,
) -> None:
    """Test that the link local address is ignored."""
    service_type = "_test._tcp.local."
    service_info = get_service_info_mock(service_type, f"test.{service_type}")
    service_info.addresses = ["169.254.12.3", "192.168.66.12"]
    info = zeroconf.info_from_service(service_info)
    assert info.host == "192.168.66.12"


async def test_info_from_service_with_unspecified_address_first(
    hass: HomeAssistant,
) -> None:
    """Test that the unspecified address is ignored."""
    service_type = "_test._tcp.local."
    service_info = get_service_info_mock(service_type, f"test.{service_type}")
    service_info.addresses = ["0.0.0.0", "192.168.66.12"]
    info = zeroconf.info_from_service(service_info)
    assert info.host == "192.168.66.12"


async def test_info_from_service_with_unspecified_address_only(
    hass: HomeAssistant,
) -> None:
    """Test that the unspecified address is ignored."""
    service_type = "_test._tcp.local."
    service_info = get_service_info_mock(service_type, f"test.{service_type}")
    service_info.addresses = ["0.0.0.0"]
    info = zeroconf.info_from_service(service_info)
    assert info is None


async def test_info_from_service_with_link_local_address_second(
    hass: HomeAssistant,
) -> None:
    """Test that the link local address is ignored."""
    service_type = "_test._tcp.local."
    service_info = get_service_info_mock(service_type, f"test.{service_type}")
    service_info.addresses = ["192.168.66.12", "169.254.12.3"]
    info = zeroconf.info_from_service(service_info)
    assert info.host == "192.168.66.12"


async def test_info_from_service_with_link_local_address_only(
    hass: HomeAssistant,
) -> None:
    """Test that the link local address is ignored."""
    service_type = "_test._tcp.local."
    service_info = get_service_info_mock(service_type, f"test.{service_type}")
    service_info.addresses = ["169.254.12.3"]
    info = zeroconf.info_from_service(service_info)
    assert info is None


async def test_info_from_service_prefers_ipv4(hass: HomeAssistant) -> None:
    """Test that ipv4 addresses are preferred."""
    service_type = "_test._tcp.local."
    service_info = get_service_info_mock(service_type, f"test.{service_type}")
    service_info.addresses = ["2001:db8:3333:4444:5555:6666:7777:8888", "192.168.66.12"]
    info = zeroconf.info_from_service(service_info)
    assert info.host == "192.168.66.12"


async def test_info_from_service_can_return_ipv6(hass: HomeAssistant) -> None:
    """Test that IPv6-only devices can be discovered."""
    service_type = "_test._tcp.local."
    service_info = get_service_info_mock(service_type, f"test.{service_type}")
    service_info.addresses = ["fd11:1111:1111:0:1234:1234:1234:1234"]
    info = zeroconf.info_from_service(service_info)
    assert info.host == "fd11:1111:1111:0:1234:1234:1234:1234"


async def test_get_instance(hass: HomeAssistant, mock_async_zeroconf: None) -> None:
    """Test we get an instance."""
    assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
    assert (
        await hass.components.zeroconf.async_get_async_instance() is mock_async_zeroconf
    )
    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    await hass.async_block_till_done()
    assert len(mock_async_zeroconf.ha_async_close.mock_calls) == 1


async def test_removed_ignored(hass: HomeAssistant, mock_async_zeroconf: None) -> None:
    """Test we remove it when a zeroconf entry is removed."""

    def service_update_mock(ipv6, zeroconf, services, handlers):
        """Call service update handler."""
        handlers[0](
            zeroconf,
            "_service.added.local.",
            "name._service.added.local.",
            ServiceStateChange.Added,
        )
        handlers[0](
            zeroconf,
            "_service.updated.local.",
            "name._service.updated.local.",
            ServiceStateChange.Updated,
        )
        handlers[0](
            zeroconf,
            "_service.removed.local.",
            "name._service.removed.local.",
            ServiceStateChange.Removed,
        )

    with patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ), patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_service_info_mock,
    ) as mock_service_info:
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_info.mock_calls) == 2
    assert mock_service_info.mock_calls[0][1][0] == "_service.added.local."
    assert mock_service_info.mock_calls[1][1][0] == "_service.updated.local."


_ADAPTER_WITH_DEFAULT_ENABLED = [
    {
        "auto": True,
        "default": True,
        "enabled": True,
        "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
        "ipv6": [],
        "name": "eth1",
    }
]


async def test_async_detect_interfaces_setting_non_loopback_route(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test without default interface and the route returns a non-loopback address."""
    with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object(
        hass.config_entries.flow, "async_init"
    ), patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ), patch(
        "homeassistant.components.zeroconf.network.async_get_adapters",
        return_value=_ADAPTER_WITH_DEFAULT_ENABLED,
    ), patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_service_info_mock,
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert mock_zc.mock_calls[0] == call(
        interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only
    )


_ADAPTERS_WITH_MANUAL_CONFIG = [
    {
        "auto": True,
        "index": 1,
        "default": False,
        "enabled": True,
        "ipv4": [],
        "ipv6": [
            {
                "address": "2001:db8::",
                "network_prefix": 64,
                "flowinfo": 1,
                "scope_id": 1,
            },
            {
                "address": "fe80::1234:5678:9abc:def0",
                "network_prefix": 64,
                "flowinfo": 1,
                "scope_id": 1,
            },
        ],
        "name": "eth0",
    },
    {
        "auto": True,
        "index": 2,
        "default": False,
        "enabled": True,
        "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
        "ipv6": [],
        "name": "eth1",
    },
    {
        "auto": True,
        "index": 3,
        "default": False,
        "enabled": True,
        "ipv4": [{"address": "172.16.1.5", "network_prefix": 23}],
        "ipv6": [
            {
                "address": "fe80::dead:beef:dead:beef",
                "network_prefix": 64,
                "flowinfo": 1,
                "scope_id": 3,
            }
        ],
        "name": "eth2",
    },
    {
        "auto": False,
        "index": 4,
        "default": False,
        "enabled": False,
        "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
        "ipv6": [],
        "name": "vtun0",
    },
]


async def test_async_detect_interfaces_setting_empty_route_linux(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test without default interface config and the route returns nothing on linux."""
    with patch("homeassistant.components.zeroconf.sys.platform", "linux"), patch(
        "homeassistant.components.zeroconf.HaZeroconf"
    ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ), patch(
        "homeassistant.components.zeroconf.network.async_get_adapters",
        return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
    ), patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_service_info_mock,
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()
    assert mock_zc.mock_calls[0] == call(
        interfaces=[
            "2001:db8::%1",
            "fe80::1234:5678:9abc:def0%1",
            "192.168.1.5",
            "172.16.1.5",
            "fe80::dead:beef:dead:beef%3",
        ],
        ip_version=IPVersion.All,
    )


async def test_async_detect_interfaces_setting_empty_route_freebsd(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test without default interface and the route returns nothing on freebsd."""
    with patch("homeassistant.components.zeroconf.sys.platform", "freebsd"), patch(
        "homeassistant.components.zeroconf.HaZeroconf"
    ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ), patch(
        "homeassistant.components.zeroconf.network.async_get_adapters",
        return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
    ), patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_service_info_mock,
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()
    assert mock_zc.mock_calls[0] == call(
        interfaces=[
            "192.168.1.5",
            "172.16.1.5",
        ],
        ip_version=IPVersion.V4Only,
    )


_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6 = [
    {
        "auto": True,
        "default": True,
        "enabled": True,
        "index": 1,
        "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
        "ipv6": [
            {
                "address": "fe80::dead:beef:dead:beef",
                "network_prefix": 64,
                "flowinfo": 1,
                "scope_id": 3,
            }
        ],
        "name": "eth1",
    }
]


async def test_async_detect_interfaces_explicitly_set_ipv6_linux(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test interfaces are explicitly set when IPv6 is present on linux."""
    with patch("homeassistant.components.zeroconf.sys.platform", "linux"), patch(
        "homeassistant.components.zeroconf.HaZeroconf"
    ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ), patch(
        "homeassistant.components.zeroconf.network.async_get_adapters",
        return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
    ), patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_service_info_mock,
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert mock_zc.mock_calls[0] == call(
        interfaces=["192.168.1.5", "fe80::dead:beef:dead:beef%3"],
        ip_version=IPVersion.All,
    )


async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test interfaces are explicitly set when IPv6 is present on freebsd."""
    with patch("homeassistant.components.zeroconf.sys.platform", "freebsd"), patch(
        "homeassistant.components.zeroconf.HaZeroconf"
    ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ), patch(
        "homeassistant.components.zeroconf.network.async_get_adapters",
        return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
    ), patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_service_info_mock,
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert mock_zc.mock_calls[0] == call(
        interfaces=InterfaceChoice.Default,
        ip_version=IPVersion.V4Only,
    )


async def test_no_name(hass: HomeAssistant, mock_async_zeroconf: None) -> None:
    """Test fallback to Home for mDNS announcement if the name is missing."""
    hass.config.location_name = ""
    with patch("homeassistant.components.zeroconf.HaZeroconf"):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
        await hass.async_block_till_done()

    register_call = mock_async_zeroconf.async_register_service.mock_calls[-1]
    info = register_call.args[0]
    assert info.name == "Home._home-assistant._tcp.local."


async def test_setup_with_disallowed_characters_in_local_name(
    hass: HomeAssistant, mock_async_zeroconf: None, caplog: pytest.LogCaptureFixture
) -> None:
    """Test we still setup with disallowed characters in the location name."""
    with patch.object(hass.config_entries.flow, "async_init"), patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
    ), patch.object(
        hass.config,
        "location_name",
        "My.House",
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
        await hass.async_block_till_done()

    calls = mock_async_zeroconf.async_register_service.mock_calls
    assert calls[0][1][0].name == "My House._home-assistant._tcp.local."


async def test_start_with_frontend(
    hass: HomeAssistant, mock_async_zeroconf: None
) -> None:
    """Test we start with the frontend."""
    with patch("homeassistant.components.zeroconf.HaZeroconf"):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "frontend"})
        await hass.async_block_till_done()

    mock_async_zeroconf.async_register_service.assert_called_once()


async def test_zeroconf_removed(hass: HomeAssistant, mock_async_zeroconf: None) -> None:
    """Test we dismiss flows when a PTR record is removed."""

    def _device_removed_mock(ipv6, zeroconf, services, handlers):
        """Call service update handler."""
        handlers[0](
            zeroconf,
            "_http._tcp.local.",
            "Shelly108._http._tcp.local.",
            ServiceStateChange.Removed,
        )

    with patch.dict(
        zc_gen.ZEROCONF,
        {
            "_http._tcp.local.": [
                {
                    "domain": "shelly",
                    "name": "shelly*",
                }
            ]
        },
        clear=True,
    ), patch.object(
        hass.config_entries.flow,
        "async_progress_by_init_data_type",
        return_value=[{"flow_id": "mock_flow_id"}],
    ) as mock_async_progress_by_init_data_type, patch.object(
        hass.config_entries.flow, "async_abort"
    ) as mock_async_abort, patch.object(
        zeroconf, "HaAsyncServiceBrowser", side_effect=_device_removed_mock
    ) as mock_service_browser, patch(
        "homeassistant.components.zeroconf.AsyncServiceInfo",
        side_effect=get_zeroconf_info_mock("FFAADDCC11DD"),
    ):
        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    assert len(mock_service_browser.mock_calls) == 1
    assert len(mock_async_progress_by_init_data_type.mock_calls) == 1
    assert mock_async_abort.mock_calls[0][1][0] == "mock_flow_id"
