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

import time

from bleak.backends.scanner import AdvertisementData, BLEDevice
import pytest

from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
    MONOTONIC_TIME,
    BaseHaRemoteScanner,
    BluetoothChange,
    BluetoothScanningMode,
    BluetoothServiceInfo,
    HaBluetoothConnector,
    async_clear_advertisement_history,
    async_scanner_by_source,
    async_scanner_devices_by_address,
)
from homeassistant.core import HomeAssistant, callback

from . import (
    FakeRemoteScanner,
    FakeScanner,
    MockBleakClient,
    _get_manager,
    generate_advertisement_data,
    generate_ble_device,
    inject_advertisement,
)


@pytest.mark.usefixtures("enable_bluetooth")
async def test_scanner_by_source(hass: HomeAssistant) -> None:
    """Test we can get a scanner by source."""

    hci2_scanner = FakeScanner("hci2", "hci2")
    cancel_hci2 = bluetooth.async_register_scanner(hass, hci2_scanner)

    assert async_scanner_by_source(hass, "hci2") is hci2_scanner
    cancel_hci2()
    assert async_scanner_by_source(hass, "hci2") is None


async def test_monotonic_time() -> None:
    """Test monotonic time."""
    assert MONOTONIC_TIME() == pytest.approx(time.monotonic(), abs=0.1)


@pytest.mark.usefixtures("enable_bluetooth")
async def test_async_get_advertisement_callback(hass: HomeAssistant) -> None:
    """Test getting advertisement callback."""
    callback = bluetooth.async_get_advertisement_callback(hass)
    assert callback is not None


@pytest.mark.usefixtures("enable_bluetooth")
async def test_async_scanner_devices_by_address_connectable(
    hass: HomeAssistant,
) -> None:
    """Test getting scanner devices by address with connectable devices."""
    manager = _get_manager()

    class FakeInjectableScanner(BaseHaRemoteScanner):
        def inject_advertisement(
            self, device: BLEDevice, advertisement_data: AdvertisementData
        ) -> None:
            """Inject an advertisement."""
            self._async_on_advertisement(
                device.address,
                advertisement_data.rssi,
                device.name,
                advertisement_data.service_uuids,
                advertisement_data.service_data,
                advertisement_data.manufacturer_data,
                advertisement_data.tx_power,
                {"scanner_specific_data": "test"},
                MONOTONIC_TIME(),
            )

    connector = (
        HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
    )
    scanner = FakeInjectableScanner("esp32", "esp32", connector, True)
    unsetup = scanner.async_setup()
    cancel = manager.async_register_scanner(scanner)
    switchbot_device = generate_ble_device(
        "44:44:33:11:23:45",
        "wohand",
        {},
    )
    switchbot_device_adv = generate_advertisement_data(
        local_name="wohand",
        service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"],
        service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"},
        manufacturer_data={1: b"\x01"},
        rssi=-100,
    )
    scanner.inject_advertisement(switchbot_device, switchbot_device_adv)
    assert async_scanner_devices_by_address(
        hass, switchbot_device.address, connectable=True
    ) == async_scanner_devices_by_address(hass, "44:44:33:11:23:45", connectable=False)
    devices = async_scanner_devices_by_address(
        hass, switchbot_device.address, connectable=False
    )
    assert len(devices) == 1
    assert devices[0].scanner == scanner
    assert devices[0].ble_device.name == switchbot_device.name
    assert devices[0].advertisement.local_name == switchbot_device_adv.local_name
    unsetup()
    cancel()


@pytest.mark.usefixtures("enable_bluetooth")
async def test_async_scanner_devices_by_address_non_connectable(
    hass: HomeAssistant,
) -> None:
    """Test getting scanner devices by address with non-connectable devices."""
    manager = _get_manager()
    switchbot_device = generate_ble_device(
        "44:44:33:11:23:45",
        "wohand",
        {},
    )
    switchbot_device_adv = generate_advertisement_data(
        local_name="wohand",
        service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"],
        service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"},
        manufacturer_data={1: b"\x01"},
        rssi=-100,
    )

    class FakeStaticScanner(FakeScanner):
        @property
        def discovered_devices(self) -> list[BLEDevice]:
            """Return a list of discovered devices."""
            return [switchbot_device]

        @property
        def discovered_devices_and_advertisement_data(
            self,
        ) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
            """Return a list of discovered devices and their advertisement data."""
            return {switchbot_device.address: (switchbot_device, switchbot_device_adv)}

    connector = (
        HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
    )
    scanner = FakeStaticScanner("esp32", "esp32", connector)
    cancel = manager.async_register_scanner(scanner)

    assert scanner.discovered_devices_and_advertisement_data == {
        switchbot_device.address: (switchbot_device, switchbot_device_adv)
    }

    assert (
        async_scanner_devices_by_address(
            hass, switchbot_device.address, connectable=True
        )
        == []
    )
    devices = async_scanner_devices_by_address(
        hass, switchbot_device.address, connectable=False
    )
    assert len(devices) == 1
    assert devices[0].scanner == scanner
    assert devices[0].ble_device.name == switchbot_device.name
    assert devices[0].advertisement.local_name == switchbot_device_adv.local_name
    cancel()


@pytest.mark.usefixtures("enable_bluetooth")
async def test_async_current_scanners(hass: HomeAssistant) -> None:
    """Test getting the list of current scanners."""
    # The enable_bluetooth fixture registers one scanner
    initial_scanners = bluetooth.async_current_scanners(hass)
    assert len(initial_scanners) == 1
    initial_scanner_count = len(initial_scanners)

    # Verify current_mode is accessible on the initial scanner
    for scanner in initial_scanners:
        assert hasattr(scanner, "current_mode")
        # The mode might be None or a BluetoothScanningMode enum value

    # Register additional connectable scanners
    hci0_scanner = FakeScanner("hci0", "hci0")
    hci1_scanner = FakeScanner("hci1", "hci1")
    cancel_hci0 = bluetooth.async_register_scanner(hass, hci0_scanner)
    cancel_hci1 = bluetooth.async_register_scanner(hass, hci1_scanner)

    # Test that the new scanners are added
    scanners = bluetooth.async_current_scanners(hass)
    assert len(scanners) == initial_scanner_count + 2
    assert hci0_scanner in scanners
    assert hci1_scanner in scanners

    # Verify current_mode is accessible on all scanners
    for scanner in scanners:
        assert hasattr(scanner, "current_mode")
        # Verify it's None or the correct type (BluetoothScanningMode)
        assert scanner.current_mode is None or isinstance(
            scanner.current_mode, BluetoothScanningMode
        )

    # Register non-connectable scanner
    connector = HaBluetoothConnector(
        MockBleakClient, "mock_bleak_client", lambda: False
    )
    hci2_scanner = FakeRemoteScanner("hci2", "hci2", connector, False)
    cancel_hci2 = bluetooth.async_register_scanner(hass, hci2_scanner)

    # Test that all scanners are returned (both connectable and non-connectable)
    all_scanners = bluetooth.async_current_scanners(hass)
    assert len(all_scanners) == initial_scanner_count + 3
    assert hci0_scanner in all_scanners
    assert hci1_scanner in all_scanners
    assert hci2_scanner in all_scanners

    # Verify current_mode is accessible on all scanners including non-connectable
    for scanner in all_scanners:
        assert hasattr(scanner, "current_mode")
        # The mode should be None or a BluetoothScanningMode instance
        assert scanner.current_mode is None or isinstance(
            scanner.current_mode, BluetoothScanningMode
        )

    # Clean up our scanners
    cancel_hci0()
    cancel_hci1()
    cancel_hci2()

    # Verify we're back to the initial scanner
    final_scanners = bluetooth.async_current_scanners(hass)
    assert len(final_scanners) == initial_scanner_count


@pytest.mark.usefixtures("enable_bluetooth")
async def test_clear_advertisement_history(hass: HomeAssistant) -> None:
    """Test clearing advertisement history bypasses the dedup guard."""
    callbacks: list[tuple[BluetoothServiceInfo, BluetoothChange]] = []

    @callback
    def _fake_subscriber(
        service_info: BluetoothServiceInfo, change: BluetoothChange
    ) -> None:
        callbacks.append((service_info, change))

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

    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"},
    )

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

    # Identical advertisement is deduplicated by the manager
    inject_advertisement(hass, switchbot_device, switchbot_adv)
    await hass.async_block_till_done()

    assert len(callbacks) == 1

    # Clearing the advertisement history makes the next identical
    # advertisement be treated as new data
    async_clear_advertisement_history(hass, "44:44:33:11:23:45")

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

    assert len(callbacks) == 2

    cancel()
