"""Tests for the Bluetooth integration."""

import asyncio
from datetime import timedelta
import sys
import time
from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock, patch

from bleak import BleakError
from bleak.backends.scanner import AdvertisementData, BLEDevice
from bluetooth_adapters import DEFAULT_ADDRESS
from habluetooth import scanner, set_manager
from habluetooth.wrappers import HaBleakScannerWrapper
import pytest

from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
    BluetoothCallbackReplay,
    BluetoothChange,
    BluetoothScanningMode,
    BluetoothServiceInfo,
    HaBluetoothConnector,
    async_clear_address_from_match_history,
    async_process_advertisements,
    async_rediscover_address,
    async_track_unavailable,
)
from homeassistant.components.bluetooth.const import (
    BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
    CONF_MODE,
    CONF_PASSIVE,
    CONF_SOURCE_CONFIG_ENTRY_ID,
    CONF_SOURCE_DOMAIN,
    CONF_SOURCE_MODEL,
    DOMAIN,
    LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
    SOURCE_LOCAL,
    UNAVAILABLE_TRACK_SECONDS,
)
from homeassistant.components.bluetooth.manager import HomeAssistantBluetoothManager
from homeassistant.components.bluetooth.match import (
    ADDRESS,
    CONNECTABLE,
    LOCAL_NAME,
    MANUFACTURER_ID,
    SERVICE_DATA_UUID,
    SERVICE_UUID,
    BluetoothCallbackMatcher,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
    CONF_SOURCE,
    EVENT_HOMEASSISTANT_STARTED,
    EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util

from . import (
    FakeRemoteScanner,
    FakeScanner,
    MockBleakClient,
    _get_manager,
    async_setup_with_default_adapter,
    async_setup_with_one_adapter,
    generate_advertisement_data,
    generate_ble_device,
    inject_advertisement,
    inject_advertisement_with_time_and_source_connectable,
    patch_discovered_devices,
)

from tests.common import MockConfigEntry, async_fire_time_changed


@pytest.mark.usefixtures("enable_bluetooth")
async def test_setup_and_stop(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test we and setup and stop the scanner."""
    mock_bt = [
        {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
    ]
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch.object(hass.config_entries.flow, "async_init"),
    ):
        assert await async_setup_component(
            hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
        )
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    await hass.async_block_till_done()
    assert len(mock_bleak_scanner_start.mock_calls) == 1


@pytest.mark.skipif(sys.platform != "linux", reason="Requires Linux BlueZ scanner")
@pytest.mark.parametrize(
    "options",
    [{CONF_MODE: "passive"}, {CONF_PASSIVE: True}],
    ids=["mode_passive", "legacy_passive_true"],
)
@pytest.mark.usefixtures("one_adapter")
async def test_setup_and_stop_passive(
    hass: HomeAssistant,
    mock_bleak_scanner_start: MagicMock,
    options: dict[str, Any],
) -> None:
    """Test we set up and stop the scanner in passive mode.

    Covers both the new CONF_MODE key and the legacy CONF_PASSIVE boolean
    so the fallback path in async_setup_entry stays exercised.
    """
    entry = MockConfigEntry(
        domain=bluetooth.DOMAIN,
        data={},
        options=options,
        unique_id="00:00:00:00:00:01",
    )
    entry.add_to_hass(hass)
    init_kwargs = None

    class MockPassiveBleakScanner:
        def __init__(self, *args: Any, **kwargs: Any) -> None:
            """Init the scanner."""
            nonlocal init_kwargs
            init_kwargs = kwargs

        async def start(self, *args, **kwargs):
            """Start the scanner."""

        async def stop(self, *args, **kwargs):
            """Stop the scanner."""

        def register_detection_callback(self, *args, **kwargs):
            """Register a callback."""

    with patch(
        "habluetooth.scanner.OriginalBleakScanner",
        MockPassiveBleakScanner,
    ):
        assert await async_setup_component(
            hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
        )
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
        await hass.async_block_till_done()

    assert init_kwargs == {
        "bluez": {
            **scanner.PASSIVE_SCANNER_ARGS,  # pylint: disable=c-extension-no-member
            "adapter": "hci0",
        },
        "scanning_mode": "passive",
    }


@pytest.mark.skipif(sys.platform != "linux", reason="Requires Linux BlueZ scanner")
async def test_setup_and_stop_old_bluez(
    hass: HomeAssistant,
    mock_bleak_scanner_start: MagicMock,
    one_adapter_old_bluez: None,
) -> None:
    """Default AUTO falls back to active on adapters without passive scan support."""
    entry = MockConfigEntry(
        domain=bluetooth.DOMAIN,
        data={},
        options={},
        unique_id="00:00:00:00:00:01",
    )
    entry.add_to_hass(hass)
    init_kwargs = None

    class MockBleakScanner:
        def __init__(self, *args: Any, **kwargs: Any) -> None:
            """Init the scanner."""
            nonlocal init_kwargs
            init_kwargs = kwargs

        async def start(self, *args, **kwargs):
            """Start the scanner."""

        async def stop(self, *args, **kwargs):
            """Stop the scanner."""

        def register_detection_callback(self, *args, **kwargs):
            """Register a callback."""

    with patch(
        "habluetooth.scanner.OriginalBleakScanner",
        MockBleakScanner,
    ):
        assert await async_setup_component(
            hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
        )
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
        await hass.async_block_till_done()

    assert init_kwargs == {
        "bluez": {"adapter": "hci0"},
        "scanning_mode": "active",
    }


@pytest.mark.usefixtures("one_adapter")
async def test_setup_and_stop_no_bluetooth(
    hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
    """Test we fail gracefully when bluetooth is not available."""
    mock_bt = [
        {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
    ]
    with (
        patch(
            "habluetooth.scanner.OriginalBleakScanner",
            side_effect=BleakError,
        ) as mock_ha_bleak_scanner,
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch("homeassistant.components.bluetooth.discovery_flow.async_create_flow"),
    ):
        await async_setup_with_one_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    await hass.async_block_till_done()
    assert len(mock_ha_bleak_scanner.mock_calls) == 1
    assert "Failed to initialize Bluetooth" in caplog.text


@pytest.mark.usefixtures("macos_adapter")
async def test_setup_and_stop_broken_bluetooth(
    hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
    """Test we fail gracefully when bluetooth/dbus is broken."""
    mock_bt = []
    with (
        patch(
            "habluetooth.scanner.OriginalBleakScanner.start",
            side_effect=BleakError,
        ),
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    await hass.async_block_till_done()
    assert "Failed to start Bluetooth" in caplog.text
    assert len(bluetooth.async_discovered_service_info(hass)) == 0


@pytest.mark.usefixtures("macos_adapter")
async def test_setup_and_stop_broken_bluetooth_hanging(
    hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
    """Test we fail gracefully when bluetooth/dbus is hanging."""
    mock_bt = []

    async def _mock_hang():
        await asyncio.sleep(1)

    with (
        patch.object(scanner, "START_TIMEOUT", 0),
        patch(
            "habluetooth.scanner.OriginalBleakScanner.start",
            side_effect=_mock_hang,
        ),
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    await hass.async_block_till_done()
    assert "Timed out starting Bluetooth" in caplog.text


@pytest.mark.usefixtures("macos_adapter")
async def test_setup_and_retry_adapter_not_yet_available(
    hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
    """Test we retry if the adapter is not yet available."""
    mock_bt = []
    with (
        patch(
            "habluetooth.scanner.OriginalBleakScanner.start",
            side_effect=BleakError,
        ),
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0]

    assert "Failed to start Bluetooth" in caplog.text
    assert len(bluetooth.async_discovered_service_info(hass)) == 0
    assert entry.state is ConfigEntryState.SETUP_RETRY

    with patch(
        "habluetooth.scanner.OriginalBleakScanner.start",
    ):
        async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10))
        await hass.async_block_till_done()
    assert entry.state is ConfigEntryState.LOADED

    with patch(
        "habluetooth.scanner.OriginalBleakScanner.stop",
    ):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
        await hass.async_block_till_done()


@pytest.mark.usefixtures("macos_adapter")
async def test_no_race_during_manual_reload_in_retry_state(
    hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
    """Test we can successfully reload when the entry is in a retry state."""
    mock_bt = []
    with (
        patch(
            "habluetooth.scanner.OriginalBleakScanner.start",
            side_effect=BleakError,
        ),
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0]

    assert "Failed to start Bluetooth" in caplog.text
    assert len(bluetooth.async_discovered_service_info(hass)) == 0
    assert entry.state is ConfigEntryState.SETUP_RETRY

    with patch(
        "habluetooth.scanner.OriginalBleakScanner.start",
    ):
        await hass.config_entries.async_reload(entry.entry_id)
        await hass.async_block_till_done()

    assert entry.state is ConfigEntryState.LOADED

    with patch(
        "habluetooth.scanner.OriginalBleakScanner.stop",
    ):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
        await hass.async_block_till_done()


@pytest.mark.usefixtures("macos_adapter")
async def test_calling_async_discovered_devices_no_bluetooth(
    hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
    """Test we fail gracefully when there is no bluetooth."""
    mock_bt = []
    with (
        patch(
            "habluetooth.scanner.OriginalBleakScanner",
            side_effect=FileNotFoundError,
        ),
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    await hass.async_block_till_done()
    assert "Failed to initialize Bluetooth" in caplog.text
    assert not bluetooth.async_discovered_service_info(hass)
    assert not bluetooth.async_address_present(hass, "aa:bb:bb:dd:ee:ff")


@pytest.mark.usefixtures("enable_bluetooth")
async def test_discovery_match_by_service_uuid(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test bluetooth discovery match by service_uuid."""
    mock_bt = [
        {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
    ]
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        wrong_device = generate_ble_device("44:44:33:11:23:45", "wrong_name")
        wrong_adv = generate_advertisement_data(
            local_name="wrong_name", service_uuids=[]
        )

        inject_advertisement(hass, wrong_device, wrong_adv)
        await hass.async_block_till_done()

        assert len(mock_config_flow.mock_calls) == 0

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
        )

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        await hass.async_block_till_done()

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


@patch.object(
    bluetooth,
    "async_get_bluetooth",
    return_value=[
        {
            "domain": "sensorpush",
            "local_name": "s",
            "service_uuid": "ef090000-11d6-42ba-93b8-9dd7ec090aa9",
        }
    ],
)
@pytest.mark.usefixtures("mock_bluetooth_adapters")
async def test_discovery_match_by_service_uuid_and_short_local_name(
    mock_async_get_bluetooth: AsyncMock,
    hass: HomeAssistant,
    mock_bleak_scanner_start: MagicMock,
) -> None:
    """Test bluetooth discovery match by service_uuid and short local name."""
    entry = MockConfigEntry(domain=DOMAIN, unique_id="00:00:00:00:00:01")
    entry.add_to_hass(hass)
    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        wrong_device = generate_ble_device("44:44:33:11:23:45", "wrong_name")
        wrong_adv = generate_advertisement_data(local_name="s", service_uuids=[])

        inject_advertisement(hass, wrong_device, wrong_adv)
        await hass.async_block_till_done()

        assert len(mock_config_flow.mock_calls) == 0

        ht1_device = generate_ble_device("44:44:33:11:23:45", "s")
        ht1_adv = generate_advertisement_data(
            local_name="s", service_uuids=["ef090000-11d6-42ba-93b8-9dd7ec090aa9"]
        )

        inject_advertisement(hass, ht1_device, ht1_adv)
        await hass.async_block_till_done()

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


def _domains_from_mock_config_flow(mock_config_flow: Mock) -> list[str]:
    """Get all the domains that were passed to async_init except bluetooth."""
    return [call[1][0] for call in mock_config_flow.mock_calls if call[1][0] != DOMAIN]


@pytest.mark.usefixtures("macos_adapter")
async def test_discovery_match_by_service_uuid_connectable(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test discovery match by service_uuid, connectable."""
    mock_bt = [
        {
            "domain": "switchbot",
            "connectable": True,
            "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
        }
    ]
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        wrong_device = generate_ble_device("44:44:33:11:23:45", "wrong_name")
        wrong_adv = generate_advertisement_data(
            local_name="wrong_name", service_uuids=[]
        )

        inject_advertisement_with_time_and_source_connectable(
            hass, wrong_device, wrong_adv, time.monotonic(), "any", True
        )
        await hass.async_block_till_done()

        assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
        )

        inject_advertisement_with_time_and_source_connectable(
            hass, switchbot_device, switchbot_adv, time.monotonic(), "any", True
        )
        await hass.async_block_till_done()

        called_domains = _domains_from_mock_config_flow(mock_config_flow)
        assert len(called_domains) == 1
        assert called_domains == ["switchbot"]


@pytest.mark.usefixtures("macos_adapter")
async def test_discovery_match_by_service_uuid_not_connectable(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test discovery match by service_uuid, not connectable."""
    mock_bt = [
        {
            "domain": "switchbot",
            "connectable": True,
            "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
        }
    ]
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        wrong_device = generate_ble_device("44:44:33:11:23:45", "wrong_name")
        wrong_adv = generate_advertisement_data(
            local_name="wrong_name", service_uuids=[]
        )

        inject_advertisement_with_time_and_source_connectable(
            hass, wrong_device, wrong_adv, time.monotonic(), "any", False
        )
        await hass.async_block_till_done()

        assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
        )

        inject_advertisement_with_time_and_source_connectable(
            hass, switchbot_device, switchbot_adv, time.monotonic(), "any", False
        )
        await hass.async_block_till_done()

        assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0


@pytest.mark.usefixtures("macos_adapter")
async def test_discovery_match_by_name_connectable_false(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test discovery match by name with non-connectable."""
    mock_bt = [
        {
            "domain": "qingping",
            "connectable": False,
            "local_name": "Qingping*",
        }
    ]
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        wrong_device = generate_ble_device("44:44:33:11:23:45", "wrong_name")
        wrong_adv = generate_advertisement_data(
            local_name="wrong_name", service_uuids=[]
        )

        inject_advertisement_with_time_and_source_connectable(
            hass, wrong_device, wrong_adv, time.monotonic(), "any", False
        )
        await hass.async_block_till_done()

        assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0

        qingping_device = generate_ble_device(
            "44:44:33:11:23:45", "Qingping Motion & Light"
        )
        qingping_adv = generate_advertisement_data(
            local_name="Qingping Motion & Light",
            service_data={
                "0000fdcd-0000-1000-8000-00805f9b34fb": (
                    b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x01{"
                )
            },
        )

        inject_advertisement_with_time_and_source_connectable(
            hass, qingping_device, qingping_adv, time.monotonic(), "any", False
        )
        await hass.async_block_till_done()

        assert _domains_from_mock_config_flow(mock_config_flow) == ["qingping"]

        mock_config_flow.reset_mock()
        # Make sure it will also take a connectable device
        qingping_adv_with_better_rssi = generate_advertisement_data(
            local_name="Qingping Motion & Light",
            service_data={
                "0000fdcd-0000-1000-8000-00805f9b34fb": (
                    b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x02{"
                )
            },
            rssi=-30,
        )
        inject_advertisement_with_time_and_source_connectable(
            hass,
            qingping_device,
            qingping_adv_with_better_rssi,
            time.monotonic(),
            "any",
            True,
        )
        await hass.async_block_till_done()
        assert _domains_from_mock_config_flow(mock_config_flow) == ["qingping"]


@pytest.mark.usefixtures("macos_adapter")
async def test_discovery_match_by_local_name(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test bluetooth discovery match by local_name."""
    mock_bt = [{"domain": "switchbot", "local_name": "wohand"}]
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        wrong_device = generate_ble_device("44:44:33:11:23:45", "wrong_name")
        wrong_adv = generate_advertisement_data(
            local_name="wrong_name", service_uuids=[]
        )

        inject_advertisement(hass, wrong_device, wrong_adv)
        await hass.async_block_till_done()

        assert len(mock_config_flow.mock_calls) == 0

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
        )

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        await hass.async_block_till_done()

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


@pytest.mark.usefixtures("enable_bluetooth")
async def test_discovery_match_by_service_uuid_when_name_changes_from_mac(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test discovery matches when name changes from MAC."""
    mock_bt = [
        {
            "domain": "improv_ble",
            "service_uuid": "00467768-6228-2272-4663-277478268000",
        }
    ]
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        # First advertisement: name is MAC address, with service UUID
        # This should trigger discovery
        device_mac_name = generate_ble_device("64:E8:33:7E:0D:9E", "64:E8:33:7E:0D:9E")
        adv_mac_name = generate_advertisement_data(
            local_name="64:E8:33:7E:0D:9E",
            service_uuids=["00467768-6228-2272-4663-277478268000"],
            service_data={
                "00004677-0000-1000-8000-00805f9b34fb": b"\x02\x00\x00\x00\x00\x00"
            },
        )

        inject_advertisement(hass, device_mac_name, adv_mac_name)
        await hass.async_block_till_done()

        assert len(mock_config_flow.mock_calls) == 1
        assert mock_config_flow.mock_calls[0][1][0] == "improv_ble"
        mock_config_flow.reset_mock()

        # Second advertisement: name changes to real name, same service UUID
        # This should trigger discovery again because the name changed
        device_real_name = generate_ble_device("64:E8:33:7E:0D:9E", "improvtest")
        adv_real_name = generate_advertisement_data(
            local_name="improvtest",
            service_uuids=["00467768-6228-2272-4663-277478268000"],
            service_data={
                "00004677-0000-1000-8000-00805f9b34fb": b"\x02\x00\x00\x00\x00\x00"
            },
        )

        inject_advertisement(hass, device_real_name, adv_real_name)
        await hass.async_block_till_done()

        # Should still match improv_ble even though name changed
        assert len(mock_config_flow.mock_calls) == 1
        assert mock_config_flow.mock_calls[0][1][0] == "improv_ble"


@pytest.mark.usefixtures("macos_adapter")
async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start."""
    mock_bt = [
        {
            "domain": "homekit_controller",
            "manufacturer_id": 76,
            "manufacturer_data_start": [0x06, 0x02, 0x03],
        }
    ]
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        hkc_device = generate_ble_device("44:44:33:11:23:45", "lock")
        hkc_adv_no_mfr_data = generate_advertisement_data(
            local_name="lock",
            service_uuids=[],
            manufacturer_data={},
        )
        hkc_adv = generate_advertisement_data(
            local_name="lock",
            service_uuids=[],
            manufacturer_data={76: b"\x06\x02\x03\x99"},
        )

        # 1st discovery with no manufacturer data
        # should not trigger config flow
        inject_advertisement(hass, hkc_device, hkc_adv_no_mfr_data)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0
        mock_config_flow.reset_mock()

        # 2nd discovery with manufacturer data
        # should trigger a config flow
        inject_advertisement(hass, hkc_device, hkc_adv)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 1
        assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller"
        mock_config_flow.reset_mock()

        # 3rd discovery should not generate another flow
        inject_advertisement(hass, hkc_device, hkc_adv)
        await hass.async_block_till_done()

        assert len(mock_config_flow.mock_calls) == 0

        mock_config_flow.reset_mock()
        not_hkc_device = generate_ble_device("44:44:33:11:23:21", "lock")
        not_hkc_adv = generate_advertisement_data(
            local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"}
        )

        inject_advertisement(hass, not_hkc_device, not_hkc_adv)
        await hass.async_block_till_done()

        assert len(mock_config_flow.mock_calls) == 0
        not_apple_device = generate_ble_device("44:44:33:11:23:23", "lock")
        not_apple_adv = generate_advertisement_data(
            local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"}
        )

        inject_advertisement(hass, not_apple_device, not_apple_adv)
        await hass.async_block_till_done()

        assert len(mock_config_flow.mock_calls) == 0


@pytest.mark.usefixtures("macos_adapter")
async def test_discovery_match_by_service_data_uuid_then_others(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test bluetooth discovery match by service_data_uuid and then other fields."""
    mock_bt = [
        {
            "domain": "my_domain",
            "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb",
        },
        {
            "domain": "my_domain",
            "service_uuid": "0000fd3d-0000-1000-8000-00805f9b34fc",
        },
        {
            "domain": "other_domain",
            "manufacturer_id": 323,
        },
    ]
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        device = generate_ble_device("44:44:33:11:23:45", "lock")
        adv_without_service_data_uuid = generate_advertisement_data(
            local_name="lock",
            service_uuids=[],
            manufacturer_data={},
        )
        adv_with_mfr_data = generate_advertisement_data(
            local_name="lock",
            service_uuids=[],
            manufacturer_data={323: b"\x01\x02\x03"},
            service_data={},
        )
        adv_with_service_data_uuid = generate_advertisement_data(
            local_name="lock",
            service_uuids=[],
            manufacturer_data={},
            service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"},
        )
        adv_with_service_data_uuid_and_mfr_data = generate_advertisement_data(
            local_name="lock",
            service_uuids=[],
            manufacturer_data={323: b"\x01\x02\x03"},
            service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"},
        )
        adv_with_service_data_uuid_and_mfr_data_and_service_uuid = (
            generate_advertisement_data(
                local_name="lock",
                manufacturer_data={323: b"\x01\x02\x03"},
                service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"},
                service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"],
            )
        )
        adv_with_service_uuid = generate_advertisement_data(
            local_name="lock",
            manufacturer_data={},
            service_data={},
            service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"],
        )
        # 1st discovery should not generate a flow because the
        # service_data_uuid is not in the advertisement
        inject_advertisement(hass, device, adv_without_service_data_uuid)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0
        mock_config_flow.reset_mock()

        # 2nd discovery should not generate a flow because the
        # service_data_uuid is not in the advertisement
        inject_advertisement(hass, device, adv_without_service_data_uuid)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0
        mock_config_flow.reset_mock()

        # 3rd discovery should generate a flow because the
        # manufacturer_data is in the advertisement
        inject_advertisement(hass, device, adv_with_mfr_data)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 1
        assert mock_config_flow.mock_calls[0][1][0] == "other_domain"
        mock_config_flow.reset_mock()

        # 4th discovery should generate a flow because the
        # service_data_uuid is in the advertisement and
        # we never saw a service_data_uuid before
        inject_advertisement(hass, device, adv_with_service_data_uuid)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 1
        assert mock_config_flow.mock_calls[0][1][0] == "my_domain"
        mock_config_flow.reset_mock()

        # 5th discovery should not generate a flow because the
        # we already saw an advertisement with the service_data_uuid
        inject_advertisement(hass, device, adv_with_service_data_uuid)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0

        # 6th discovery should not generate a flow because the
        # manufacturer_data is in the advertisement
        # and we saw manufacturer_data before
        inject_advertisement(hass, device, adv_with_service_data_uuid_and_mfr_data)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0
        mock_config_flow.reset_mock()

        # 7th discovery should generate a flow because the
        # service_uuids is in the advertisement
        # and we never saw service_uuids before
        inject_advertisement(
            hass, device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid
        )
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 2
        assert {
            mock_config_flow.mock_calls[0][1][0],
            mock_config_flow.mock_calls[1][1][0],
        } == {"my_domain", "other_domain"}
        mock_config_flow.reset_mock()

        # 8th discovery should not generate a flow
        # since all fields have been seen at this point
        inject_advertisement(
            hass, device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid
        )
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0
        mock_config_flow.reset_mock()

        # 9th discovery should not generate a flow
        # since all fields have been seen at this point
        inject_advertisement(hass, device, adv_with_service_uuid)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0

        # 10th discovery should not generate a flow
        # since all fields have been seen at this point
        inject_advertisement(hass, device, adv_with_service_data_uuid)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0

        # 11th discovery should not generate a flow
        # since all fields have been seen at this point
        inject_advertisement(hass, device, adv_without_service_data_uuid)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0


@pytest.mark.usefixtures("macos_adapter")
async def test_discovery_match_by_service_data_uuid_when_format_changes(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test bluetooth discovery match by service_data_uuid when format changes."""
    mock_bt = [
        {
            "domain": "xiaomi_ble",
            "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb",
        },
        {
            "domain": "qingping",
            "service_data_uuid": "0000fdcd-0000-1000-8000-00805f9b34fb",
        },
    ]
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        device = generate_ble_device("44:44:33:11:23:45", "lock")
        adv_without_service_data_uuid = generate_advertisement_data(
            local_name="Qingping Temp RH M",
            service_uuids=[],
            manufacturer_data={},
        )
        xiaomi_format_adv = generate_advertisement_data(
            local_name="Qingping Temp RH M",
            service_data={
                "0000fe95-0000-1000-8000-00805f9b34fb": b"0XH\x0b\x06\xa7%\x144-X\x08"
            },
        )
        qingping_format_adv = generate_advertisement_data(
            local_name="Qingping Temp RH M",
            service_data={
                "0000fdcd-0000-1000-8000-00805f9b34fb": (
                    b"\x08\x16\xa7%\x144-X\x01\x04\xdb\x00\xa6\x01\x02\x01d"
                )
            },
        )
        # 1st discovery should not generate a flow because the
        # service_data_uuid is not in the advertisement
        inject_advertisement(hass, device, adv_without_service_data_uuid)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0
        mock_config_flow.reset_mock()

        # 2nd discovery should generate a flow because the
        # service_data_uuid matches xiaomi format
        inject_advertisement(hass, device, xiaomi_format_adv)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 1
        assert mock_config_flow.mock_calls[0][1][0] == "xiaomi_ble"
        mock_config_flow.reset_mock()

        # 4th discovery should generate a flow because the
        # service_data_uuid matches qingping format
        inject_advertisement(hass, device, qingping_format_adv)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 1
        assert mock_config_flow.mock_calls[0][1][0] == "qingping"
        mock_config_flow.reset_mock()

        # 5th discovery should not generate a flow because the
        # we already saw an advertisement with the service_data_uuid
        inject_advertisement(hass, device, qingping_format_adv)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0
        mock_config_flow.reset_mock()

        # 6th discovery should not generate a flow because the
        # we already saw an advertisement with the service_data_uuid
        inject_advertisement(hass, device, xiaomi_format_adv)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0
        mock_config_flow.reset_mock()


@pytest.mark.usefixtures("macos_adapter")
async def test_discovery_match_by_service_data_uuid_bthome(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test bluetooth discovery match by service_data_uuid for bthome."""
    mock_bt = [
        {
            "domain": "bthome",
            "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb",
        },
    ]
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        device = generate_ble_device("44:44:33:11:23:45", "Shelly Button")
        button_adv = generate_advertisement_data(
            local_name="Shelly Button",
            service_uuids=[],
            manufacturer_data={},
            service_data={"0000fcd2-0000-1000-8000-00805f9b34fb": b"@\x00k\x01d:\x01"},
        )
        # 1st discovery should generate a flow because the service data uuid matches
        inject_advertisement(hass, device, button_adv)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 1
        mock_config_flow.reset_mock()

        # 2nd discovery should not generate a flow because the
        # we already saw an advertisement with the service_data_uuid
        inject_advertisement(hass, device, button_adv)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0
        mock_config_flow.reset_mock()


@pytest.mark.usefixtures("macos_adapter")
async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test discovery matches for service_uuid then mfr_id."""
    mock_bt = [
        {
            "domain": "my_domain",
            "manufacturer_id": 76,
        },
        {
            "domain": "my_domain",
            "service_uuid": "0000fd3d-0000-1000-8000-00805f9b34fc",
        },
    ]
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        device = generate_ble_device("44:44:33:11:23:45", "lock")
        adv_service_uuids = generate_advertisement_data(
            local_name="lock",
            service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fc"],
            manufacturer_data={},
        )
        adv_manufacturer_data = generate_advertisement_data(
            local_name="lock",
            service_uuids=[],
            manufacturer_data={76: b"\x06\x02\x03\x99"},
        )

        # 1st discovery with matches service_uuid
        # should trigger config flow
        inject_advertisement(hass, device, adv_service_uuids)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 1
        assert mock_config_flow.mock_calls[0][1][0] == "my_domain"
        mock_config_flow.reset_mock()

        # 2nd discovery with manufacturer data
        # should trigger a config flow
        inject_advertisement(hass, device, adv_manufacturer_data)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 1
        assert mock_config_flow.mock_calls[0][1][0] == "my_domain"
        mock_config_flow.reset_mock()

        # 3rd discovery should not generate another flow
        inject_advertisement(hass, device, adv_service_uuids)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0

        # 4th discovery should not generate another flow
        inject_advertisement(hass, device, adv_manufacturer_data)
        await hass.async_block_till_done()
        assert len(mock_config_flow.mock_calls) == 0


@pytest.mark.usefixtures("enable_bluetooth")
async def test_rediscovery(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test bluetooth discovery can be re-enabled for a given domain."""
    mock_bt = [
        {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
    ]
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
        )
        switchbot_adv_2 = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={1: b"\x01"},
        )
        inject_advertisement(hass, switchbot_device, switchbot_adv)
        await hass.async_block_till_done()

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        await hass.async_block_till_done()

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

        async_rediscover_address(hass, "44:44:33:11:23:45")

        inject_advertisement(hass, switchbot_device, switchbot_adv_2)
        await hass.async_block_till_done()

        assert len(mock_config_flow.mock_calls) == 3
        assert mock_config_flow.mock_calls[1][1][0] == "switchbot"


@pytest.mark.usefixtures("enable_bluetooth")
async def test_clear_address_from_match_history(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test clearing match history without re-triggering discovery."""
    mock_bt = [
        {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"}
    ]
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
    ):
        await async_setup_with_default_adapter(hass)
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
        )
        switchbot_adv_2 = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={1: b"\x01"},
        )
        inject_advertisement(hass, switchbot_device, switchbot_adv)
        await hass.async_block_till_done()

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        await hass.async_block_till_done()

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

        # Clear match history - should NOT trigger immediate rediscovery
        async_clear_address_from_match_history(hass, "44:44:33:11:23:45")
        await hass.async_block_till_done()

        # No new discovery should have been triggered
        assert len(mock_config_flow.mock_calls) == 1

        # But when we inject new advertisement with
        # different data, it should be discovered
        inject_advertisement(hass, switchbot_device, switchbot_adv_2)
        await hass.async_block_till_done()

        # Now discovery should happen because history was cleared and data changed
        assert len(mock_config_flow.mock_calls) == 2
        assert mock_config_flow.mock_calls[1][1][0] == "switchbot"


@pytest.mark.usefixtures("macos_adapter")
async def test_async_discovered_device_api(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test the async_discovered_device API."""
    mock_bt = []
    set_manager(None)
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch(
            # Must patch before we setup
            "bleak.BleakScanner.discovered_devices_and_advertisement_data",
            {
                "44:44:33:11:23:45": (
                    MagicMock(address="44:44:33:11:23:45"),
                    MagicMock(),
                )
            },
        ),
    ):
        with pytest.raises(RuntimeError, match="BluetoothManager has not been set"):
            assert not bluetooth.async_discovered_service_info(hass)
        with pytest.raises(RuntimeError, match="BluetoothManager has not been set"):
            assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22")
        await async_setup_with_default_adapter(hass)

        with patch.object(hass.config_entries.flow, "async_init"):
            hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
            await hass.async_block_till_done()

            assert len(mock_bleak_scanner_start.mock_calls) == 1

            assert not bluetooth.async_discovered_service_info(hass)

            wrong_device = generate_ble_device("44:44:33:11:23:42", "wrong_name")
            wrong_adv = generate_advertisement_data(
                local_name="wrong_name", service_uuids=[]
            )
            inject_advertisement(hass, wrong_device, wrong_adv)
            switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
            switchbot_adv = generate_advertisement_data(
                local_name="wohand", service_uuids=[]
            )
            inject_advertisement(hass, switchbot_device, switchbot_adv)
            wrong_device_went_unavailable = False
            switchbot_device_went_unavailable = False

            @callback
            def _wrong_device_unavailable_callback(_address: str) -> None:
                """Wrong device unavailable callback."""
                nonlocal wrong_device_went_unavailable
                wrong_device_went_unavailable = True
                raise ValueError("blow up")

            @callback
            def _switchbot_device_unavailable_callback(_address: str) -> None:
                """Switchbot device unavailable callback."""
                nonlocal switchbot_device_went_unavailable
                switchbot_device_went_unavailable = True

            wrong_device_unavailable_cancel = async_track_unavailable(
                hass, _wrong_device_unavailable_callback, wrong_device.address
            )
            switchbot_device_unavailable_cancel = async_track_unavailable(
                hass, _switchbot_device_unavailable_callback, switchbot_device.address
            )

            async_fire_time_changed(
                hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
            )
            await hass.async_block_till_done()

            service_infos = bluetooth.async_discovered_service_info(hass)
            assert switchbot_device_went_unavailable is False
            assert wrong_device_went_unavailable is True

            # See the devices again
            inject_advertisement(hass, wrong_device, wrong_adv)
            inject_advertisement(hass, switchbot_device, switchbot_adv)
            # Cancel the callbacks
            wrong_device_unavailable_cancel()
            switchbot_device_unavailable_cancel()
            wrong_device_went_unavailable = False
            switchbot_device_went_unavailable = False

            # Verify the cancel is effective
            async_fire_time_changed(
                hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
            )
            await hass.async_block_till_done()
            assert switchbot_device_went_unavailable is False
            assert wrong_device_went_unavailable is False

            assert len(service_infos) == 1
            # wrong_name should not appear because bleak no longer sees it
            infos = list(service_infos)
            assert infos[0].name == "wohand"
            assert infos[0].source == SOURCE_LOCAL
            assert isinstance(infos[0].device, BLEDevice)
            assert isinstance(infos[0].advertisement, AdvertisementData)

            assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False
            assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callbacks(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo,
        change: BluetoothChange,
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch.object(hass.config_entries.flow, "async_init"),
    ):
        await async_setup_with_default_adapter(hass)

        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        seen_switchbot_device = generate_ble_device("44:44:33:11:23:46", "wohand")
        seen_switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )

        inject_advertisement(hass, seen_switchbot_device, seen_switchbot_adv)

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {SERVICE_UUID: "cba20d00-224d-11e6-9fb8-0002a5d5c51b"},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )

        inject_advertisement(hass, switchbot_device, switchbot_adv)

        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        inject_advertisement(hass, empty_device, empty_adv)
        await hass.async_block_till_done()

        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        inject_advertisement(hass, empty_device, empty_adv)
        await hass.async_block_till_done()

        cancel()

        inject_advertisement(hass, empty_device, empty_adv)
        await hass.async_block_till_done()

    assert len(callbacks) == 2

    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "wohand"
    assert service_info.source == SOURCE_LOCAL
    assert service_info.manufacturer == "Nordic Semiconductor ASA"
    assert service_info.manufacturer_id == 89


@pytest.mark.parametrize(
    ("devices", "matcher", "replay", "expected_addresses"),
    [
        pytest.param(
            [
                ("AA:BB:CC:DD:EE:01", "older", 1000.0),
                ("AA:BB:CC:DD:EE:02", "newer", 2000.0),
            ],
            None,
            BluetoothCallbackReplay.OLDEST_FIRST,
            ["AA:BB:CC:DD:EE:01", "AA:BB:CC:DD:EE:02"],
            id="oldest-first",
        ),
        pytest.param(
            [
                ("AA:BB:CC:DD:EE:01", "older", 1000.0),
                ("AA:BB:CC:DD:EE:02", "newer", 2000.0),
            ],
            None,
            BluetoothCallbackReplay.NEWEST_FIRST,
            ["AA:BB:CC:DD:EE:02", "AA:BB:CC:DD:EE:01"],
            id="newest-first",
        ),
        pytest.param(
            [
                ("AA:BB:CC:DD:EE:01", "target", 1000.0),
                ("AA:BB:CC:DD:EE:02", "other", 2000.0),
                ("AA:BB:CC:DD:EE:03", "target", 3000.0),
            ],
            {LOCAL_NAME: "target"},
            BluetoothCallbackReplay.NEWEST_FIRST,
            ["AA:BB:CC:DD:EE:03", "AA:BB:CC:DD:EE:01"],
            id="newest-first-with-filter",
        ),
        pytest.param(
            [
                ("AA:BB:CC:DD:EE:01", "older", 1000.0),
                ("AA:BB:CC:DD:EE:02", "newer", 2000.0),
            ],
            None,
            BluetoothCallbackReplay.DISABLED,
            [],
            id="disabled",
        ),
        pytest.param(
            [
                ("AA:BB:CC:DD:EE:01", "target", 1000.0),
                ("AA:BB:CC:DD:EE:02", "other", 2000.0),
            ],
            {ADDRESS: "AA:BB:CC:DD:EE:01"},
            BluetoothCallbackReplay.NEWEST_FIRST,
            ["AA:BB:CC:DD:EE:01"],
            id="newest-first-address-match",
        ),
        pytest.param(
            [
                ("AA:BB:CC:DD:EE:01", "newer", 2000.0),
                ("AA:BB:CC:DD:EE:02", "older", 1000.0),
            ],
            None,
            BluetoothCallbackReplay.OLDEST_FIRST,
            ["AA:BB:CC:DD:EE:02", "AA:BB:CC:DD:EE:01"],
            id="oldest-first-out-of-order-insertion",
        ),
    ],
)
@pytest.mark.usefixtures("enable_bluetooth", "mock_bleak_scanner_start")
async def test_register_callbacks_history_replay_order(
    hass: HomeAssistant,
    devices: list[tuple[str, str, float]],
    matcher: BluetoothCallbackMatcher | None,
    replay: BluetoothCallbackReplay,
    expected_addresses: list[str],
) -> None:
    """History replay respects the replay order kwarg."""
    mock_bt = []
    replayed: list[BluetoothServiceInfo] = []

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        for address, name, adv_time in devices:
            device = generate_ble_device(address, name)
            adv = generate_advertisement_data(local_name=name)
            inject_advertisement_with_time_and_source_connectable(
                hass, device, adv, adv_time, SOURCE_LOCAL, True
            )

        def _subscriber(
            service_info: BluetoothServiceInfo, change: BluetoothChange
        ) -> None:
            replayed.append(service_info)

        cancel = bluetooth.async_register_callback(
            hass, _subscriber, matcher, BluetoothScanningMode.ACTIVE, replay=replay
        )
        cancel()

    assert [si.address for si in replayed] == expected_addresses


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callbacks_raises_exception(
    hass: HomeAssistant,
    mock_bleak_scanner_start: MagicMock,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test registering a callback that raises ValueError."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo,
        change: BluetoothChange,
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))
        raise ValueError

    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch.object(hass.config_entries.flow, "async_init"),
    ):
        await async_setup_with_default_adapter(hass)

        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {SERVICE_UUID: "cba20d00-224d-11e6-9fb8-0002a5d5c51b"},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )

        inject_advertisement(hass, switchbot_device, switchbot_adv)

        cancel()

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        await hass.async_block_till_done()

    assert len(callbacks) == 1

    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "wohand"
    assert service_info.source == SOURCE_LOCAL
    assert service_info.manufacturer == "Nordic Semiconductor ASA"
    assert service_info.manufacturer_id == 89

    assert "ValueError" in caplog.text


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_address(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by address."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))
        if len(callbacks) >= 3:
            raise ValueError

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {"address": "44:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )

        inject_advertisement(hass, switchbot_device, switchbot_adv)

        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        inject_advertisement(hass, empty_device, empty_adv)
        await hass.async_block_till_done()

        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        # 3rd callback raises ValueError but is still tracked
        inject_advertisement(hass, empty_device, empty_adv)
        await hass.async_block_till_done()

        cancel()

        # 4th callback should not be tracked since we canceled
        inject_advertisement(hass, empty_device, empty_adv)
        await hass.async_block_till_done()

        # Now register again with a callback that fails to
        # make sure we do not perm fail
        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {ADDRESS: "44:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
        )
        cancel()

        # Now register again, since the 3rd callback
        # should fail but we should still record it
        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {ADDRESS: "44:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
        )
        cancel()

    assert len(callbacks) == 3

    for idx in range(3):
        service_info: BluetoothServiceInfo = callbacks[idx][0]
        assert service_info.name == "wohand"
        assert service_info.manufacturer == "Nordic Semiconductor ASA"
        assert service_info.manufacturer_id == 89


@pytest.mark.parametrize(
    ("matcher", "mode", "kwargs", "expected_args"),
    [
        pytest.param(
            {"address": "44:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
            {"scan_interval": 300.0, "scan_duration": 5.0},
            ("44:44:33:11:23:45", 300.0, 5.0),
            id="active_with_interval_and_duration",
        ),
        pytest.param(
            {"address": "44:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
            {"scan_interval": 300.0},
            ("44:44:33:11:23:45", 300.0, None),
            id="active_with_interval_default_duration",
        ),
        pytest.param(
            {"address": "44:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
            {},
            ("44:44:33:11:23:45", None, None),
            id="active_with_address_default_cadence",
        ),
    ],
)
@pytest.mark.usefixtures("enable_bluetooth", "mock_bleak_scanner_start")
async def test_register_callback_registers_active_scan(
    hass: HomeAssistant,
    matcher: dict[str, str],
    mode: BluetoothScanningMode,
    kwargs: dict[str, float],
    expected_args: tuple[str, float | None, float | None],
) -> None:
    """An address matcher in non-PASSIVE mode registers an active-scan request."""
    mock_bt: list[Any] = []
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        def _cb(_si: BluetoothServiceInfo, _ch: BluetoothChange) -> None:
            return None

        mock_cancel = Mock()
        with patch.object(
            HomeAssistantBluetoothManager,
            "async_register_active_scan",
            return_value=mock_cancel,
        ) as mock_register:
            cancel = bluetooth.async_register_callback(
                hass, _cb, matcher, mode, **kwargs
            )
            mock_register.assert_called_once_with(*expected_args)
            mock_cancel.assert_not_called()
            cancel()
            mock_cancel.assert_called_once()


@pytest.mark.parametrize(
    ("matcher", "mode", "kwargs"),
    [
        pytest.param(
            {"address": "44:44:33:11:23:45"},
            BluetoothScanningMode.PASSIVE,
            {"scan_interval": 300.0},
            id="passive_mode",
        ),
        pytest.param(
            {SERVICE_UUID: "cba20d00-224d-11e6-9fb8-0002a5d5c51b"},
            BluetoothScanningMode.ACTIVE,
            {"scan_interval": 300.0},
            id="no_address_in_matcher",
        ),
    ],
)
@pytest.mark.usefixtures("enable_bluetooth", "mock_bleak_scanner_start")
async def test_register_callback_skips_active_scan(
    hass: HomeAssistant,
    matcher: dict[str, str],
    mode: BluetoothScanningMode,
    kwargs: dict[str, float],
) -> None:
    """PASSIVE mode or a matcher without an address never registers an active scan."""
    mock_bt: list[Any] = []
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        def _cb(_si: BluetoothServiceInfo, _ch: BluetoothChange) -> None:
            return None

        with patch.object(
            HomeAssistantBluetoothManager, "async_register_active_scan"
        ) as mock_register:
            cancel = bluetooth.async_register_callback(
                hass, _cb, matcher, mode, **kwargs
            )
            mock_register.assert_not_called()
            cancel()


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_address_connectable_only(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by address connectable only."""
    mock_bt = []
    connectable_callbacks = []
    non_connectable_callbacks = []

    def _fake_connectable_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        connectable_callbacks.append((service_info, change))

    def _fake_non_connectable_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        non_connectable_callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_connectable_subscriber,
            {ADDRESS: "44:44:33:11:23:45", CONNECTABLE: True},
            BluetoothScanningMode.ACTIVE,
        )
        cancel2 = bluetooth.async_register_callback(
            hass,
            _fake_non_connectable_subscriber,
            {ADDRESS: "44:44:33:11:23:45", CONNECTABLE: False},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        switchbot_adv_better_rssi = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
            rssi=-30,
        )
        inject_advertisement_with_time_and_source_connectable(
            hass, switchbot_device, switchbot_adv, time.monotonic(), "test", False
        )
        inject_advertisement_with_time_and_source_connectable(
            hass,
            switchbot_device,
            switchbot_adv_better_rssi,
            time.monotonic(),
            "test",
            True,
        )

        cancel()
        cancel2()

    assert len(connectable_callbacks) == 1
    # Non connectable will take either a connectable
    # or non-connectable device
    assert len(non_connectable_callbacks) == 2


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_manufacturer_id(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by manufacturer_id."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {MANUFACTURER_ID: 21},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        apple_device = generate_ble_device("44:44:33:11:23:45", "rtx")
        apple_adv = generate_advertisement_data(
            local_name="rtx",
            manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
        )

        inject_advertisement(hass, apple_device, apple_adv)

        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        inject_advertisement(hass, empty_device, empty_adv)
        await hass.async_block_till_done()

        cancel()

    assert len(callbacks) == 1

    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "rtx"
    assert service_info.manufacturer == "RTX Telecom A/S"
    assert service_info.manufacturer_id == 21


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_connectable(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by connectable."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {CONNECTABLE: False},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        apple_device = generate_ble_device("44:44:33:11:23:45", "rtx")
        apple_adv = generate_advertisement_data(
            local_name="rtx",
            manufacturer_data={7676: b"\xd8.\xad\xcd\r\x85"},
        )

        inject_advertisement(hass, apple_device, apple_adv)

        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        inject_advertisement(hass, empty_device, empty_adv)
        await hass.async_block_till_done()

        cancel()

    assert len(callbacks) == 2

    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "rtx"
    service_info: BluetoothServiceInfo = callbacks[1][0]
    assert service_info.name == "empty"


@pytest.mark.usefixtures("enable_bluetooth")
async def test_not_filtering_wanted_apple_devices(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test filtering noisy apple devices."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {MANUFACTURER_ID: 76},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        ibeacon_device = generate_ble_device("44:44:33:11:23:45", "rtx")
        ibeacon_adv = generate_advertisement_data(
            local_name="ibeacon",
            manufacturer_data={76: b"\x02\x00\x00\x00"},
        )

        inject_advertisement(hass, ibeacon_device, ibeacon_adv)

        homekit_device = generate_ble_device("44:44:33:11:23:46", "rtx")
        homekit_adv = generate_advertisement_data(
            local_name="homekit",
            manufacturer_data={76: b"\x06\x00\x00\x00"},
        )

        inject_advertisement(hass, homekit_device, homekit_adv)

        apple_device = generate_ble_device("44:44:33:11:23:47", "rtx")
        apple_adv = generate_advertisement_data(
            local_name="apple",
            manufacturer_data={76: b"\x10\x00\x00\x00"},
        )

        inject_advertisement(hass, apple_device, apple_adv)

        cancel()

    assert len(callbacks) == 3


@pytest.mark.usefixtures("enable_bluetooth")
async def test_filtering_noisy_apple_devices(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test filtering noisy apple devices."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {MANUFACTURER_ID: 21},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        apple_device = generate_ble_device("44:44:33:11:23:45", "rtx")
        apple_adv = generate_advertisement_data(
            local_name="noisy",
            manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"},
        )

        inject_advertisement(hass, apple_device, apple_adv)

        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        inject_advertisement(hass, empty_device, empty_adv)
        await hass.async_block_till_done()

        cancel()

    assert len(callbacks) == 0


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_address_connectable_manufacturer_id(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by address, manufacturer_id, and connectable."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {MANUFACTURER_ID: 21, CONNECTABLE: False, ADDRESS: "44:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        apple_device = generate_ble_device("44:44:33:11:23:45", "rtx")
        apple_adv = generate_advertisement_data(
            local_name="rtx",
            manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
        )

        inject_advertisement(hass, apple_device, apple_adv)

        apple_device_wrong_address = generate_ble_device("44:44:33:11:23:46", "rtx")

        inject_advertisement(hass, apple_device_wrong_address, apple_adv)
        await hass.async_block_till_done()

        cancel()

    assert len(callbacks) == 1

    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "rtx"
    assert service_info.manufacturer == "RTX Telecom A/S"
    assert service_info.manufacturer_id == 21


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_manufacturer_id_and_address(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by manufacturer_id and address."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {MANUFACTURER_ID: 21, ADDRESS: "44:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        rtx_device = generate_ble_device("44:44:33:11:23:45", "rtx")
        rtx_adv = generate_advertisement_data(
            local_name="rtx",
            manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
        )

        inject_advertisement(hass, rtx_device, rtx_adv)

        yale_device = generate_ble_device("44:44:33:11:23:45", "apple")
        yale_adv = generate_advertisement_data(
            local_name="yale",
            manufacturer_data={465: b"\xd8.\xad\xcd\r\x85"},
        )

        inject_advertisement(hass, yale_device, yale_adv)
        await hass.async_block_till_done()

        other_apple_device = generate_ble_device("44:44:33:11:23:22", "apple")
        other_apple_adv = generate_advertisement_data(
            local_name="apple",
            manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
        )
        inject_advertisement(hass, other_apple_device, other_apple_adv)

        cancel()

    assert len(callbacks) == 1

    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "rtx"
    assert service_info.manufacturer == "RTX Telecom A/S"
    assert service_info.manufacturer_id == 21


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_service_uuid_and_address(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by service_uuid and address."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {
                SERVICE_UUID: "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
                ADDRESS: "44:44:33:11:23:45",
            },
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        switchbot_dev = generate_ble_device("44:44:33:11:23:45", "switchbot")
        switchbot_adv = generate_advertisement_data(
            local_name="switchbot",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
        )

        inject_advertisement(hass, switchbot_dev, switchbot_adv)

        switchbot_missing_service_uuid_dev = generate_ble_device(
            "44:44:33:11:23:45", "switchbot"
        )
        switchbot_missing_service_uuid_adv = generate_advertisement_data(
            local_name="switchbot",
        )

        inject_advertisement(
            hass, switchbot_missing_service_uuid_dev, switchbot_missing_service_uuid_adv
        )
        await hass.async_block_till_done()

        service_uuid_wrong_address_dev = generate_ble_device(
            "44:44:33:11:23:22", "switchbot2"
        )
        service_uuid_wrong_address_adv = generate_advertisement_data(
            local_name="switchbot2",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
        )
        inject_advertisement(
            hass, service_uuid_wrong_address_dev, service_uuid_wrong_address_adv
        )

        cancel()

    assert len(callbacks) == 1

    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "switchbot"


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_service_data_uuid_and_address(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by service_data_uuid and address."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {
                SERVICE_DATA_UUID: "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
                ADDRESS: "44:44:33:11:23:45",
            },
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        switchbot_dev = generate_ble_device("44:44:33:11:23:45", "switchbot")
        switchbot_adv = generate_advertisement_data(
            local_name="switchbot",
            service_data={"cba20d00-224d-11e6-9fb8-0002a5d5c51b": b"x"},
        )

        inject_advertisement(hass, switchbot_dev, switchbot_adv)

        switchbot_missing_service_uuid_dev = generate_ble_device(
            "44:44:33:11:23:45", "switchbot"
        )
        switchbot_missing_service_uuid_adv = generate_advertisement_data(
            local_name="switchbot",
        )

        inject_advertisement(
            hass, switchbot_missing_service_uuid_dev, switchbot_missing_service_uuid_adv
        )
        await hass.async_block_till_done()

        service_uuid_wrong_address_dev = generate_ble_device(
            "44:44:33:11:23:22", "switchbot2"
        )
        service_uuid_wrong_address_adv = generate_advertisement_data(
            local_name="switchbot2",
            service_data={"cba20d00-224d-11e6-9fb8-0002a5d5c51b": b"x"},
        )
        inject_advertisement(
            hass, service_uuid_wrong_address_dev, service_uuid_wrong_address_adv
        )

        cancel()

    assert len(callbacks) == 1

    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "switchbot"


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_local_name(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by local_name."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {LOCAL_NAME: "rtx"},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        rtx_device = generate_ble_device("44:44:33:11:23:45", "rtx")
        rtx_adv = generate_advertisement_data(
            local_name="rtx",
            manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
        )

        inject_advertisement(hass, rtx_device, rtx_adv)

        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        inject_advertisement(hass, empty_device, empty_adv)

        rtx_device_2 = generate_ble_device("44:44:33:11:23:45", "rtx")
        rtx_adv_2 = generate_advertisement_data(
            local_name="rtx2",
            manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
        )
        inject_advertisement(hass, rtx_device_2, rtx_adv_2)

        await hass.async_block_till_done()

        cancel()

    assert len(callbacks) == 1

    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "rtx"
    assert service_info.manufacturer == "RTX Telecom A/S"
    assert service_info.manufacturer_id == 21


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_local_name_overly_broad(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by local_name that is too broad."""
    mock_bt = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with pytest.raises(ValueError):
        bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {LOCAL_NAME: "ab*"},
            BluetoothScanningMode.ACTIVE,
        )


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_service_data_uuid(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by service_data_uuid."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        cancel = bluetooth.async_register_callback(
            hass,
            _fake_subscriber,
            {SERVICE_DATA_UUID: "0000fe95-0000-1000-8000-00805f9b34fb"},
            BluetoothScanningMode.ACTIVE,
        )

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        apple_device = generate_ble_device("44:44:33:11:23:45", "xiaomi")
        apple_adv = generate_advertisement_data(
            local_name="xiaomi",
            service_data={
                "0000fe95-0000-1000-8000-00805f9b34fb": b"\xd8.\xad\xcd\r\x85"
            },
        )

        inject_advertisement(hass, apple_device, apple_adv)

        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        inject_advertisement(hass, empty_device, empty_adv)
        await hass.async_block_till_done()

        cancel()

    assert len(callbacks) == 1

    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "xiaomi"


@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_survives_reload(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test registering a callback by address survives bluetooth being reloaded."""
    mock_bt = []
    callbacks = []

    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        """Fake subscriber for the BleakScanner."""
        callbacks.append((service_info, change))

    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
    ):
        await async_setup_with_default_adapter(hass)

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
    await hass.async_block_till_done()

    cancel = bluetooth.async_register_callback(
        hass,
        _fake_subscriber,
        {"address": "44:44:33:11:23:45"},
        BluetoothScanningMode.ACTIVE,
    )

    assert len(mock_bleak_scanner_start.mock_calls) == 1

    switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
    switchbot_adv = generate_advertisement_data(
        local_name="wohand",
        service_uuids=["zba20d00-224d-11e6-9fb8-0002a5d5c51b"],
        manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
        service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
    )
    switchbot_adv_2 = generate_advertisement_data(
        local_name="wohand",
        service_uuids=["zba20d00-224d-11e6-9fb8-0002a5d5c51b"],
        manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
        service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
    )
    inject_advertisement(hass, switchbot_device, switchbot_adv)
    assert len(callbacks) == 1
    service_info: BluetoothServiceInfo = callbacks[0][0]
    assert service_info.name == "wohand"
    assert service_info.manufacturer == "Nordic Semiconductor ASA"
    assert service_info.manufacturer_id == 89

    entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0]
    await hass.config_entries.async_reload(entry.entry_id)
    await hass.async_block_till_done()

    inject_advertisement(hass, switchbot_device, switchbot_adv_2)
    assert len(callbacks) == 2
    service_info: BluetoothServiceInfo = callbacks[1][0]
    assert service_info.name == "wohand"
    assert service_info.manufacturer == "Nordic Semiconductor ASA"
    assert service_info.manufacturer_id == 89
    cancel()


@pytest.mark.usefixtures("enable_bluetooth")
async def test_process_advertisements_bail_on_good_advertisement(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test as soon as we see a 'good' advertisement we return it."""
    done = asyncio.Future()

    def _callback(service_info: BluetoothServiceInfo) -> bool:
        done.set_result(None)
        return len(service_info.service_data) > 0

    handle = hass.async_create_task(
        async_process_advertisements(
            hass,
            _callback,
            {"address": "aa:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
            5,
        )
    )

    while not done.done():
        device = generate_ble_device("aa:44:33:11:23:45", "wohand")
        adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fa": b"H\x10c"},
        )

        inject_advertisement(hass, device, adv)
        inject_advertisement(hass, device, adv)
        inject_advertisement(hass, device, adv)

        await asyncio.sleep(0)

    result = await handle
    assert result.name == "wohand"


@pytest.mark.usefixtures("enable_bluetooth")
async def test_process_advertisements_ignore_bad_advertisement(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Check that we ignore bad advertisements."""
    done = asyncio.Event()
    return_value = asyncio.Event()

    device = generate_ble_device("aa:44:33:11:23:45", "wohand")
    adv = generate_advertisement_data(
        local_name="wohand",
        service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"],
        manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
        service_data={"00000d00-0000-1000-8000-00805f9b34fa": b""},
    )
    adv2 = generate_advertisement_data(
        local_name="wohand",
        service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"],
        manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
        service_data={"00000d00-0000-1000-8000-00805f9b34fa": b""},
    )

    def _callback(service_info: BluetoothServiceInfo) -> bool:
        done.set()
        return return_value.is_set()

    handle = hass.async_create_task(
        async_process_advertisements(
            hass,
            _callback,
            {"address": "aa:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
            5,
        )
    )

    # The goal of this loop is to make sure that
    # async_process_advertisements sees at least one
    # callback that returns False
    while not done.is_set():
        inject_advertisement(hass, device, adv)
        inject_advertisement(hass, device, adv2)
        await asyncio.sleep(0)

    # Set the return value and mutate the advertisement
    # Check that scan ends and correct advertisement data is returned
    return_value.set()
    adv.service_data["00000d00-0000-1000-8000-00805f9b34fa"] = b"H\x10c"
    inject_advertisement(hass, device, adv)
    inject_advertisement(hass, device, adv2)
    await asyncio.sleep(0)

    result = await handle
    assert result.service_data["00000d00-0000-1000-8000-00805f9b34fa"] == b"H\x10c"


@pytest.mark.usefixtures("enable_bluetooth")
async def test_process_advertisements_timeout(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test we timeout if no advertisements at all."""

    def _callback(service_info: BluetoothServiceInfo) -> bool:
        return False

    with pytest.raises(TimeoutError):
        await async_process_advertisements(
            hass, _callback, {}, BluetoothScanningMode.ACTIVE, 0
        )


@pytest.mark.usefixtures("enable_bluetooth", "mock_bleak_scanner_start")
async def test_process_advertisements_triggers_active_scan_of_correct_duration(
    hass: HomeAssistant,
) -> None:
    """async_process_advertisements triggers active scan now."""

    def _callback(service_info: BluetoothServiceInfo) -> bool:
        return False

    timeout = 0.001
    mock_cancel = Mock()

    with (
        patch.object(
            HomeAssistantBluetoothManager,
            "async_register_active_scan",
            return_value=mock_cancel,
        ) as mock_register,
        patch.object(
            HomeAssistantBluetoothManager, "async_request_active_scan"
        ) as mock_request_active_scan,
        pytest.raises(TimeoutError),
    ):
        await async_process_advertisements(
            hass,
            _callback,
            {"address": "aa:44:33:11:23:45"},
            BluetoothScanningMode.ACTIVE,
            timeout,
        )
    mock_register.assert_called_once_with("aa:44:33:11:23:45", None, None)
    mock_request_active_scan.assert_called_once_with(timeout)
    mock_cancel.assert_called_once()


@pytest.mark.usefixtures("enable_bluetooth")
async def test_wrapped_instance_with_filter(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test wrapped instance with a filter works like BleakScanner."""
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        detected = []

        def _device_detected(
            device: BLEDevice, advertisement_data: AdvertisementData
        ) -> None:
            """Handle a detected device."""
            detected.append((device, advertisement_data))

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        switchbot_adv_2 = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        assert _get_manager() is not None
        scanner = HaBleakScannerWrapper(
            filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]}
        )
        scanner.register_detection_callback(_device_detected)

        inject_advertisement(hass, switchbot_device, switchbot_adv_2)
        await hass.async_block_till_done()

        discovered = await scanner.discover(timeout=0)
        assert len(discovered) == 1
        assert discovered == [switchbot_device]
        assert len(detected) == 1

        scanner.register_detection_callback(_device_detected)
        # We should get a reply from the history when we register again
        assert len(detected) == 2
        scanner.register_detection_callback(_device_detected)
        # We should get a reply from the history when we register again
        assert len(detected) == 3

        with patch_discovered_devices([]):
            discovered = await scanner.discover(timeout=0)
            assert len(discovered) == 0
            assert discovered == []

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        assert len(detected) == 4

        # The filter we created in the wrapped scanner with should be respected
        # and we should not get another callback
        inject_advertisement(hass, empty_device, empty_adv)
        assert len(detected) == 4


@pytest.mark.usefixtures("enable_bluetooth")
async def test_wrapped_instance_with_service_uuids(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test wrapped instance with service_uuids list."""
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        detected = []

        def _device_detected(
            device: BLEDevice, advertisement_data: AdvertisementData
        ) -> None:
            """Handle a detected device."""
            detected.append((device, advertisement_data))

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        switchbot_adv_2 = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        assert _get_manager() is not None
        scanner = HaBleakScannerWrapper(
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
        )
        scanner.register_detection_callback(_device_detected)

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        inject_advertisement(hass, switchbot_device, switchbot_adv_2)

        await hass.async_block_till_done()

        assert len(detected) == 2

        # The UUIDs list we created in the wrapped scanner with should be respected
        # and we should not get another callback
        inject_advertisement(hass, empty_device, empty_adv)
        assert len(detected) == 2


@pytest.mark.usefixtures("enable_bluetooth")
async def test_wrapped_instance_with_service_uuids_with_coro_callback(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test wrapped instance with service_uuids list.

    Verify that coro callbacks are supported.
    """
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        detected = []

        async def _device_detected(
            device: BLEDevice, advertisement_data: AdvertisementData
        ) -> None:
            """Handle a detected device."""
            detected.append((device, advertisement_data))

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        switchbot_adv_2 = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        assert _get_manager() is not None
        scanner = HaBleakScannerWrapper(
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
        )
        scanner.register_detection_callback(_device_detected)

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        inject_advertisement(hass, switchbot_device, switchbot_adv_2)

        await hass.async_block_till_done()

        assert len(detected) == 2

        # The UUIDs list we created in the wrapped scanner with should be respected
        # and we should not get another callback
        inject_advertisement(hass, empty_device, empty_adv)
        assert len(detected) == 2


@pytest.mark.usefixtures("enable_bluetooth")
async def test_wrapped_instance_with_broken_callbacks(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test broken callbacks do not cause the scanner to fail."""
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
        ),
        patch.object(hass.config_entries.flow, "async_init"),
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        detected = []

        def _device_detected(
            device: BLEDevice, advertisement_data: AdvertisementData
        ) -> None:
            """Handle a detected device."""
            if detected:
                raise ValueError
            detected.append((device, advertisement_data))

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )

        assert _get_manager() is not None
        scanner = HaBleakScannerWrapper(
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
        )
        scanner.register_detection_callback(_device_detected)

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        await hass.async_block_till_done()
        inject_advertisement(hass, switchbot_device, switchbot_adv)
        await hass.async_block_till_done()
        assert len(detected) == 1


@pytest.mark.usefixtures("enable_bluetooth")
async def test_wrapped_instance_changes_uuids(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test consumers can use the wrapped instance can change the uuids later."""
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()
        detected = []

        def _device_detected(
            device: BLEDevice, advertisement_data: AdvertisementData
        ) -> None:
            """Handle a detected device."""
            detected.append((device, advertisement_data))

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        switchbot_adv_2 = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        empty_device = generate_ble_device("11:22:33:44:55:66", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        assert _get_manager() is not None
        scanner = HaBleakScannerWrapper()
        scanner.set_scanning_filter(
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
        )
        scanner.register_detection_callback(_device_detected)

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        inject_advertisement(hass, switchbot_device, switchbot_adv_2)
        await hass.async_block_till_done()

        assert len(detected) == 2

        # The UUIDs list we created in the wrapped scanner with should be respected
        # and we should not get another callback
        inject_advertisement(hass, empty_device, empty_adv)
        assert len(detected) == 2


@pytest.mark.usefixtures("enable_bluetooth")
async def test_wrapped_instance_changes_filters(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test consumers can use the wrapped instance can change the filter later."""
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()
        detected = []

        def _device_detected(
            device: BLEDevice, advertisement_data: AdvertisementData
        ) -> None:
            """Handle a detected device."""
            detected.append((device, advertisement_data))

        switchbot_device = generate_ble_device("44:44:33:11:23:42", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        switchbot_adv_2 = generate_advertisement_data(
            local_name="wohand",
            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
            manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
        )
        empty_device = generate_ble_device("11:22:33:44:55:62", "empty")
        empty_adv = generate_advertisement_data(local_name="empty")

        assert _get_manager() is not None
        scanner = HaBleakScannerWrapper()
        scanner.set_scanning_filter(
            filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]}
        )
        scanner.register_detection_callback(_device_detected)

        inject_advertisement(hass, switchbot_device, switchbot_adv)
        inject_advertisement(hass, switchbot_device, switchbot_adv_2)

        await hass.async_block_till_done()

        assert len(detected) == 2

        # The UUIDs list we created in the wrapped scanner with should be respected
        # and we should not get another callback
        inject_advertisement(hass, empty_device, empty_adv)
        assert len(detected) == 2


@pytest.mark.usefixtures("enable_bluetooth")
async def test_wrapped_instance_unsupported_filter(
    hass: HomeAssistant,
    mock_bleak_scanner_start: MagicMock,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test we want when their filter is ineffective."""
    with patch(
        "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[]
    ):
        await async_setup_with_default_adapter(hass)

    with patch.object(hass.config_entries.flow, "async_init"):
        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()
        assert _get_manager() is not None
        scanner = HaBleakScannerWrapper()
        scanner.set_scanning_filter(
            filters={
                "unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
                "DuplicateData": True,
            }
        )
        assert "Only UUIDs filters are supported" in caplog.text


@pytest.mark.usefixtures("macos_adapter")
async def test_async_ble_device_from_address(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test the async_ble_device_from_address api."""
    set_manager(None)
    mock_bt = []
    with (
        patch(
            "homeassistant.components.bluetooth.async_get_bluetooth",
            return_value=mock_bt,
        ),
        patch(
            # Must patch before we setup
            "bleak.BleakScanner.discovered_devices_and_advertisement_data",
            {
                "44:44:33:11:23:45": (
                    MagicMock(address="44:44:33:11:23:45"),
                    MagicMock(),
                )
            },
        ),
    ):
        with pytest.raises(RuntimeError, match="BluetoothManager has not been set"):
            assert not bluetooth.async_discovered_service_info(hass)
        with pytest.raises(RuntimeError, match="BluetoothManager has not been set"):
            assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22")
        with pytest.raises(RuntimeError, match="BluetoothManager has not been set"):
            assert (
                bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45")
                is None
            )

        await async_setup_with_default_adapter(hass)

        hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
        await hass.async_block_till_done()

        assert len(mock_bleak_scanner_start.mock_calls) == 1

        assert not bluetooth.async_discovered_service_info(hass)

        switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
        switchbot_adv = generate_advertisement_data(
            local_name="wohand", service_uuids=[]
        )
        inject_advertisement(hass, switchbot_device, switchbot_adv)
        await hass.async_block_till_done()

        assert (
            bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45")
            is switchbot_device
        )

        assert (
            bluetooth.async_ble_device_from_address(hass, "00:66:33:22:11:22") is None
        )


@pytest.mark.usefixtures("macos_adapter")
async def test_can_unsetup_bluetooth_single_adapter_macos(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test we can setup and unsetup bluetooth."""
    entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}, unique_id=DEFAULT_ADDRESS)
    entry.add_to_hass(hass)

    for _ in range(2):
        assert await hass.config_entries.async_setup(entry.entry_id)
        await hass.async_block_till_done()

        assert await hass.config_entries.async_unload(entry.entry_id)
        await hass.async_block_till_done()


@pytest.mark.usefixtures("one_adapter")
async def test_default_address_config_entries_removed_linux(
    hass: HomeAssistant,
    mock_bleak_scanner_start: MagicMock,
) -> None:
    """Test default address entries are removed on linux."""
    entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}, unique_id=DEFAULT_ADDRESS)
    entry.add_to_hass(hass)
    await async_setup_component(hass, bluetooth.DOMAIN, {})
    await hass.async_block_till_done()
    assert not hass.config_entries.async_entries(bluetooth.DOMAIN)


@pytest.mark.usefixtures("one_adapter")
async def test_can_unsetup_bluetooth_single_adapter_linux(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test we can setup and unsetup bluetooth."""
    entry = MockConfigEntry(
        domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01"
    )
    entry.add_to_hass(hass)

    for _ in range(2):
        assert await hass.config_entries.async_setup(entry.entry_id)
        await hass.async_block_till_done()

        assert await hass.config_entries.async_unload(entry.entry_id)
        await hass.async_block_till_done()


@pytest.mark.usefixtures("two_adapters")
async def test_can_unsetup_bluetooth_multiple_adapters(
    hass: HomeAssistant,
    mock_bleak_scanner_start: MagicMock,
) -> None:
    """Test we can setup and unsetup bluetooth with multiple adapters."""
    # Setup bluetooth first since otherwise loading the first
    # config entry will load the second one as well
    await async_setup_component(hass, bluetooth.DOMAIN, {})
    await hass.async_block_till_done()

    entry1 = MockConfigEntry(
        domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01"
    )
    entry1.add_to_hass(hass)

    entry2 = MockConfigEntry(
        domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:02"
    )
    entry2.add_to_hass(hass)

    for _ in range(2):
        for entry in (entry1, entry2):
            assert await hass.config_entries.async_setup(entry.entry_id)
            await hass.async_block_till_done()

            assert await hass.config_entries.async_unload(entry.entry_id)
            await hass.async_block_till_done()


@pytest.mark.usefixtures("enable_bluetooth", "two_adapters")
async def test_three_adapters_one_missing(
    hass: HomeAssistant,
    mock_bleak_scanner_start: MagicMock,
) -> None:
    """Test three adapters but one is missing results in a retry on setup."""
    entry = MockConfigEntry(
        domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:03"
    )
    entry.add_to_hass(hass)
    assert not await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()
    assert entry.state is ConfigEntryState.SETUP_RETRY


@pytest.mark.usefixtures("one_adapter")
async def test_auto_detect_bluetooth_adapters_linux(hass: HomeAssistant) -> None:
    """Test we auto detect bluetooth adapters on linux."""
    assert await async_setup_component(hass, bluetooth.DOMAIN, {})
    await hass.async_block_till_done()
    assert not hass.config_entries.async_entries(bluetooth.DOMAIN)
    assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1


@pytest.mark.usefixtures("two_adapters")
async def test_auto_detect_bluetooth_adapters_linux_multiple(
    hass: HomeAssistant,
) -> None:
    """Test we auto detect bluetooth adapters on linux with multiple adapters."""
    assert await async_setup_component(hass, bluetooth.DOMAIN, {})
    await hass.async_block_till_done()
    assert not hass.config_entries.async_entries(bluetooth.DOMAIN)
    assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2


async def test_auto_detect_bluetooth_adapters_skips_crashed(
    hass: HomeAssistant, crashed_adapter: None
) -> None:
    """Test we skip crashed adapters on linux."""
    assert await async_setup_component(hass, bluetooth.DOMAIN, {})
    await hass.async_block_till_done()
    assert not hass.config_entries.async_entries(bluetooth.DOMAIN)
    assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0


async def test_auto_detect_bluetooth_adapters_linux_none_found(
    hass: HomeAssistant,
) -> None:
    """Test we auto detect bluetooth adapters on linux with no adapters found."""
    with (
        patch("bluetooth_adapters.systems.platform.system", return_value="Linux"),
        patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"),
        patch(
            "bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
            {},
        ),
    ):
        assert await async_setup_component(hass, bluetooth.DOMAIN, {})
        await hass.async_block_till_done()
    assert not hass.config_entries.async_entries(bluetooth.DOMAIN)
    assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0


async def test_auto_detect_bluetooth_adapters_macos(hass: HomeAssistant) -> None:
    """Test we auto detect bluetooth adapters on macos."""
    with patch("bluetooth_adapters.systems.platform.system", return_value="Darwin"):
        assert await async_setup_component(hass, bluetooth.DOMAIN, {})
        await hass.async_block_till_done()
    assert not hass.config_entries.async_entries(bluetooth.DOMAIN)
    assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1


async def test_no_auto_detect_bluetooth_adapters_windows(hass: HomeAssistant) -> None:
    """Test we auto detect bluetooth adapters on windows."""
    with patch(
        "bluetooth_adapters.systems.platform.system",
        return_value="Windows",
    ):
        assert await async_setup_component(hass, bluetooth.DOMAIN, {})
        await hass.async_block_till_done()
    assert not hass.config_entries.async_entries(bluetooth.DOMAIN)
    assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0


@pytest.mark.usefixtures("enable_bluetooth")
async def test_getting_the_scanner_returns_the_wrapped_instance(
    hass: HomeAssistant,
) -> None:
    """Test getting the scanner returns the wrapped instance."""
    scanner = bluetooth.async_get_scanner(hass)
    assert isinstance(scanner, HaBleakScannerWrapper)


@pytest.mark.usefixtures("enable_bluetooth")
async def test_scanner_count_connectable(hass: HomeAssistant) -> None:
    """Test getting the connectable scanner count."""
    scanner = FakeScanner("any", "any")
    cancel = bluetooth.async_register_scanner(hass, scanner)
    assert bluetooth.async_scanner_count(hass, connectable=True) == 1
    cancel()


@pytest.mark.usefixtures("enable_bluetooth")
async def test_scanner_remove(hass: HomeAssistant) -> None:
    """Test permanently removing a scanner."""
    scanner = FakeScanner("any", "any")
    cancel = bluetooth.async_register_scanner(hass, scanner)
    assert bluetooth.async_scanner_count(hass, connectable=True) == 1
    device = generate_ble_device("44:44:33:11:23:45", "name")
    adv = generate_advertisement_data(local_name="name", service_uuids=[])
    inject_advertisement_with_time_and_source_connectable(
        hass, device, adv, time.monotonic(), scanner.source, True
    )
    cancel()
    bluetooth.async_remove_scanner(hass, scanner.source)
    manager: HomeAssistantBluetoothManager = _get_manager()
    assert not manager.storage.async_get_advertisement_history(scanner.source)


@pytest.mark.usefixtures("enable_bluetooth")
async def test_scanner_count(hass: HomeAssistant) -> None:
    """Test getting the connectable and non-connectable scanner count."""
    scanner = FakeScanner("any", "any")
    cancel = bluetooth.async_register_scanner(hass, scanner)
    assert bluetooth.async_scanner_count(hass, connectable=False) == 2
    cancel()


@pytest.mark.usefixtures("macos_adapter")
async def test_migrate_single_entry_macos(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test we can migrate a single entry on MacOS."""
    entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={})
    entry.add_to_hass(hass)
    assert await async_setup_component(hass, bluetooth.DOMAIN, {})
    await hass.async_block_till_done()
    assert entry.unique_id == DEFAULT_ADDRESS


@pytest.mark.usefixtures("one_adapter")
async def test_migrate_single_entry_linux(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test we can migrate a single entry on Linux."""
    entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={})
    entry.add_to_hass(hass)
    assert await async_setup_component(hass, bluetooth.DOMAIN, {})
    await hass.async_block_till_done()
    assert entry.unique_id == "00:00:00:00:00:01"


@pytest.mark.usefixtures("one_adapter")
async def test_discover_new_usb_adapters(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test we can discover new usb adapters."""
    entry = MockConfigEntry(
        domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01"
    )
    entry.add_to_hass(hass)

    saved_callback = None

    def _async_register_scan_request_callback(_hass, _callback):
        nonlocal saved_callback
        saved_callback = _callback
        return lambda: None

    with patch(
        "homeassistant.components.bluetooth.usb.async_register_scan_request_callback",
        _async_register_scan_request_callback,
    ):
        assert await async_setup_component(hass, bluetooth.DOMAIN, {})
        await hass.async_block_till_done()

    assert not hass.config_entries.flow.async_progress(DOMAIN)

    saved_callback()
    assert not hass.config_entries.flow.async_progress(DOMAIN)

    with (
        patch("bluetooth_adapters.systems.platform.system", return_value="Linux"),
        patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"),
        patch(
            "bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
            {
                "hci0": {
                    "address": "00:00:00:00:00:01",
                    "hw_version": "usb:v1D6Bp0246d053F",
                    "passive_scan": False,
                    "sw_version": "homeassistant",
                    "manufacturer": "ACME",
                },
                "hci1": {
                    "address": "00:00:00:00:00:02",
                    "hw_version": "usb:v1D6Bp0246d053F",
                    "passive_scan": False,
                    "sw_version": "homeassistant",
                    "manufacturer": "ACME",
                },
            },
        ),
    ):
        for wait_sec in range(10, 20):
            async_fire_time_changed(
                hass, dt_util.utcnow() + timedelta(seconds=wait_sec)
            )
            await hass.async_block_till_done()

    assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1


@pytest.mark.usefixtures("one_adapter")
async def test_discover_new_usb_adapters_with_firmware_fallback_delay(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test we can discover new usb adapters with a firmware fallback delay."""
    entry = MockConfigEntry(
        domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01"
    )
    entry.add_to_hass(hass)

    saved_callback = None

    def _async_register_scan_request_callback(_hass, _callback):
        nonlocal saved_callback
        saved_callback = _callback
        return lambda: None

    with patch(
        "homeassistant.components.bluetooth.usb.async_register_scan_request_callback",
        _async_register_scan_request_callback,
    ):
        assert await async_setup_component(hass, bluetooth.DOMAIN, {})
        await hass.async_block_till_done()

    assert not hass.config_entries.flow.async_progress(DOMAIN)

    saved_callback()
    assert not hass.config_entries.flow.async_progress(DOMAIN)

    with (
        patch("bluetooth_adapters.systems.platform.system", return_value="Linux"),
        patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"),
        patch(
            "bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
            {},
        ),
    ):
        async_fire_time_changed(
            hass, dt_util.utcnow() + timedelta(BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2)
        )
        await hass.async_block_till_done()

    assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 0

    with (
        patch("bluetooth_adapters.systems.platform.system", return_value="Linux"),
        patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"),
        patch(
            "bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
            {
                "hci0": {
                    "address": "00:00:00:00:00:01",
                    "hw_version": "usb:v1D6Bp0246d053F",
                    "passive_scan": False,
                    "sw_version": "homeassistant",
                    "manufacturer": "ACME",
                },
                "hci1": {
                    "address": "00:00:00:00:00:02",
                    "hw_version": "usb:v1D6Bp0246d053F",
                    "passive_scan": False,
                    "sw_version": "homeassistant",
                    "manufacturer": "ACME",
                },
            },
        ),
    ):
        async_fire_time_changed(
            hass,
            dt_util.utcnow()
            + timedelta(
                seconds=LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS
                + (BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2)
            ),
        )
        await hass.async_block_till_done()

    assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1


@pytest.mark.usefixtures("no_adapters")
async def test_issue_outdated_haos_removed(
    hass: HomeAssistant,
    mock_bleak_scanner_start: MagicMock,
    operating_system_85: None,
    issue_registry: ir.IssueRegistry,
) -> None:
    """Test we do not create an issue on outdated haos anymore."""
    assert await async_setup_component(hass, bluetooth.DOMAIN, {})
    await hass.async_block_till_done()
    hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
    await hass.async_block_till_done()

    issue = issue_registry.async_get_issue(DOMAIN, "haos_outdated")
    assert issue is None


@pytest.mark.usefixtures("one_adapter")
async def test_haos_9_or_later(
    hass: HomeAssistant,
    mock_bleak_scanner_start: MagicMock,
    operating_system_90: None,
    issue_registry: ir.IssueRegistry,
) -> None:
    """Test we do not create issues for haos 9.x or later."""
    entry = MockConfigEntry(
        domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01"
    )
    entry.add_to_hass(hass)
    assert await async_setup_component(hass, bluetooth.DOMAIN, {})
    await hass.async_block_till_done()
    hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
    await hass.async_block_till_done()
    issue = issue_registry.async_get_issue(DOMAIN, "haos_outdated")
    assert issue is None


@pytest.mark.usefixtures("one_adapter")
async def test_title_updated_if_mac_address(
    hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
    """Test the title is updated if it is the mac address."""
    entry = MockConfigEntry(
        domain=DOMAIN, title="00:00:00:00:00:01", unique_id="00:00:00:00:00:01"
    )
    entry.add_to_hass(hass)
    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()
    assert entry.title == "ACME Bluetooth Adapter 5.0 (00:00:00:00:00:01)"


@pytest.mark.usefixtures("enable_bluetooth")
async def test_cleanup_orphened_remote_scanner_config_entry(
    hass: HomeAssistant,
) -> None:
    """Test the remote scanner config entries get cleaned up when orphened."""
    connector = (
        HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
    )
    scanner = FakeRemoteScanner("esp32", "esp32", connector, True)

    entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_SOURCE: scanner.source,
            CONF_SOURCE_DOMAIN: "test",
            CONF_SOURCE_MODEL: "test",
            CONF_SOURCE_CONFIG_ENTRY_ID: "no_longer_exists",
        },
        unique_id=scanner.source,
    )
    entry.add_to_hass(hass)
    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()

    # Orphened remote scanner config entry should be cleaned up
    assert not hass.config_entries.async_entry_for_domain_unique_id(
        "bluetooth", scanner.source
    )


@pytest.mark.usefixtures("enable_bluetooth")
async def test_fix_incorrect_mac_remote_scanner_config_entry(
    hass: HomeAssistant,
) -> None:
    """Test the remote scanner config entries can replace a incorrect mac."""
    source_entry = MockConfigEntry(domain="test")
    source_entry.add_to_hass(hass)
    connector = (
        HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
    )
    scanner = FakeRemoteScanner("AA:BB:CC:DD:EE:FF", "esp32", connector, True)
    assert scanner.source == "AA:BB:CC:DD:EE:FF"
    entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_SOURCE: scanner.source,
            CONF_SOURCE_DOMAIN: "test",
            CONF_SOURCE_MODEL: "test",
            CONF_SOURCE_CONFIG_ENTRY_ID: source_entry.entry_id,
        },
        unique_id=scanner.source,
    )
    entry.add_to_hass(hass)
    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()
    assert hass.config_entries.async_entry_for_domain_unique_id(
        "bluetooth", scanner.source
    )
    await hass.config_entries.async_unload(entry.entry_id)

    new_scanner = FakeRemoteScanner("AA:BB:CC:DD:EE:AA", "esp32", connector, True)
    assert new_scanner.source == "AA:BB:CC:DD:EE:AA"
    hass.config_entries.async_update_entry(
        entry,
        data={**entry.data, CONF_SOURCE: new_scanner.source},
        unique_id=new_scanner.source,
    )

    await hass.config_entries.async_setup(entry.entry_id)
    await hass.async_block_till_done()
    assert hass.config_entries.async_entry_for_domain_unique_id(
        "bluetooth", new_scanner.source
    )
    # Incorrect connection should be removed
    assert not hass.config_entries.async_entry_for_domain_unique_id(
        "bluetooth", scanner.source
    )
