"""Test bluetooth diagnostics."""

import sys
from unittest.mock import ANY, MagicMock, patch

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

from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
    MONOTONIC_TIME,
    BaseHaRemoteScanner,
    HaBluetoothConnector,
    HaScanner,
)
from homeassistant.core import HomeAssistant

from . import (
    FakeScannerMixin,
    MockBleakClient,
    _get_manager,
    generate_advertisement_data,
    generate_ble_device,
    inject_advertisement,
)

from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator


class FakeHaScanner(FakeScannerMixin, HaScanner):
    """Fake HaScanner."""

    @property
    def discovered_devices_and_advertisement_data(self):
        """Return the discovered devices and advertisement data."""
        return {
            "44:44:33:11:23:45": (
                generate_ble_device(name="x", address="44:44:33:11:23:45"),
                generate_advertisement_data(local_name="x"),
            )
        }


@pytest.mark.skipif(sys.platform != "linux", reason="Requires Linux Bluetooth stack")
@patch("homeassistant.components.bluetooth.HaScanner", FakeHaScanner)
@pytest.mark.usefixtures("enable_bluetooth", "two_adapters")
async def test_diagnostics(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    mock_bleak_scanner_start: MagicMock,
) -> None:
    """Test we can setup and unsetup bluetooth with multiple adapters."""
    # Normally we do not want to patch our classes, but since bleak will import
    # a different scanner based on the operating system, we need to patch here
    # because we cannot import the scanner class directly without it throwing an
    # error if the test is not running on linux since we won't have the correct
    # deps installed when testing on MacOS.

    with (
        patch(
            "homeassistant.components.bluetooth.diagnostics.platform.system",
            return_value="Linux",
        ),
        patch(
            "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects",
            return_value={
                "org.bluez": {
                    "/org/bluez/hci0": {
                        "org.bluez.Adapter1": {
                            "Name": "BlueZ 5.63",
                            "Alias": "BlueZ 5.63",
                            "Modalias": "usb:v1D6Bp0246d0540",
                            "Discovering": False,
                        },
                        "org.bluez.AdvertisementMonitorManager1": {
                            "SupportedMonitorTypes": ["or_patterns"],
                            "SupportedFeatures": [],
                        },
                    }
                }
            },
        ),
    ):
        entry2 = MockConfigEntry(
            domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:02"
        )
        entry2.add_to_hass(hass)

        assert await hass.config_entries.async_setup(entry2.entry_id)
        await hass.async_block_till_done()

        diag = await get_diagnostics_for_config_entry(hass, hass_client, entry2)
        expected = {
            "adapters": {
                "hci0": {
                    "address": "00:00:00:00:00:01",
                    "connection_slots": 1,
                    "hw_version": "usb:v1D6Bp0246d053F",
                    "manufacturer": "ACME",
                    "passive_scan": False,
                    "product": "Bluetooth Adapter 5.0",
                    "product_id": "aa01",
                    "sw_version": ANY,
                    "vendor_id": "cc01",
                },
                "hci1": {
                    "address": "00:00:00:00:00:02",
                    "connection_slots": 2,
                    "hw_version": "usb:v1D6Bp0246d053F",
                    "manufacturer": "ACME",
                    "passive_scan": True,
                    "product": "Bluetooth Adapter 5.0",
                    "product_id": "aa01",
                    "sw_version": ANY,
                    "vendor_id": "cc01",
                },
            },
            "dbus": {
                "org.bluez": {
                    "/org/bluez/hci0": {
                        "org.bluez.Adapter1": {
                            "Alias": "BlueZ 5.63",
                            "Discovering": False,
                            "Modalias": "usb:v1D6Bp0246d0540",
                            "Name": "BlueZ 5.63",
                        },
                        "org.bluez.AdvertisementMonitorManager1": {
                            "SupportedFeatures": [],
                            "SupportedMonitorTypes": ["or_patterns"],
                        },
                    }
                }
            },
            "manager": {
                "allocations": {
                    "00:00:00:00:00:01": {
                        "allocated": [],
                        "free": 5,
                        "slots": 5,
                        "source": "00:00:00:00:00:01",
                    },
                    "00:00:00:00:00:02": {
                        "allocated": [],
                        "free": 2,
                        "slots": 2,
                        "source": "00:00:00:00:00:02",
                    },
                },
                "adapters": {
                    "hci0": {
                        "address": "00:00:00:00:00:01",
                        "connection_slots": 1,
                        "hw_version": "usb:v1D6Bp0246d053F",
                        "manufacturer": "ACME",
                        "passive_scan": False,
                        "product": "Bluetooth Adapter 5.0",
                        "product_id": "aa01",
                        "sw_version": "homeassistant",
                        "vendor_id": "cc01",
                    },
                    "hci1": {
                        "address": "00:00:00:00:00:02",
                        "connection_slots": 2,
                        "hw_version": "usb:v1D6Bp0246d053F",
                        "manufacturer": "ACME",
                        "passive_scan": True,
                        "product": "Bluetooth Adapter 5.0",
                        "product_id": "aa01",
                        "sw_version": "homeassistant",
                        "vendor_id": "cc01",
                    },
                },
                "advertisement_tracker": {
                    "fallback_intervals": {},
                    "intervals": {},
                    "sources": {},
                    "timings": {},
                },
                "all_history": [],
                "auto_scheduler": {
                    "monotonic_time": ANY,
                    "requests": {},
                    "running": True,
                    "workers": {
                        "00:00:00:00:00:02": {
                            "failed_window": False,
                            "last_window_at": 0.0,
                            "last_window_duration": 0.0,
                            "name": "hci1 (00:00:00:00:00:02)",
                            "next_event_at": ANY,
                            "next_sweep_at": ANY,
                            "sweep_last_completed": ANY,
                            "warned_no_fallback": False,
                            "window_end": 0.0,
                        },
                    },
                },
                "connectable_history": [],
                "scanners": [
                    {
                        "adapter": "hci0",
                        "connect_completed_total": 0,
                        "connect_failed_total": 0,
                        "connect_failures": {},
                        "connect_in_progress": {},
                        "connectable": True,
                        "discovered_devices_and_advertisement_data": [],
                        "last_connect_completed_time": 0.0,
                        "last_detection": ANY,
                        "monotonic_time": ANY,
                        "name": "hci0 (00:00:00:00:00:01)",
                        "scanning": True,
                        "source": "00:00:00:00:00:01",
                        "start_time": ANY,
                        "type": "HaScanner",
                        "current_mode": {
                            "__type": "<enum 'BluetoothScanningMode'>",
                            "repr": "<BluetoothScanningMode.ACTIVE: 'active'>",
                        },
                        "requested_mode": {
                            "__type": "<enum 'BluetoothScanningMode'>",
                            "repr": "<BluetoothScanningMode.ACTIVE: 'active'>",
                        },
                    },
                    {
                        "adapter": "hci1",
                        "connect_completed_total": 0,
                        "connect_failed_total": 0,
                        "connect_failures": {},
                        "connect_in_progress": {},
                        "last_connect_completed_time": 0.0,
                        "discovered_devices_and_advertisement_data": [
                            {
                                "address": "44:44:33:11:23:45",
                                "advertisement_data": [
                                    "x",
                                    {},
                                    {},
                                    [],
                                    -127,
                                    -127,
                                    [[]],
                                ],
                                "details": None,
                                "name": "x",
                                "rssi": -127,
                            }
                        ],
                        "connectable": True,
                        "last_detection": ANY,
                        "monotonic_time": ANY,
                        "name": "hci1 (00:00:00:00:00:02)",
                        "scanning": True,
                        "source": "00:00:00:00:00:02",
                        "start_time": ANY,
                        "type": "FakeHaScanner",
                        "current_mode": {
                            "__type": "<enum 'BluetoothScanningMode'>",
                            "repr": "<BluetoothScanningMode.PASSIVE: 'passive'>",
                        },
                        "requested_mode": {
                            "__type": "<enum 'BluetoothScanningMode'>",
                            "repr": "<BluetoothScanningMode.AUTO: 'auto'>",
                        },
                    },
                ],
                "slot_manager": {
                    "adapter_slots": {"hci0": 5, "hci1": 2},
                    "allocations_by_adapter": {"hci0": [], "hci1": []},
                    "manager": False,
                },
            },
        }
        diag_scanners = diag["manager"].pop("scanners")
        expected_scanners = expected["manager"].pop("scanners")
        assert diag == expected
        assert sorted(diag_scanners, key=lambda x: x["name"]) == sorted(
            expected_scanners, key=lambda x: x["name"]
        )


@patch("homeassistant.components.bluetooth.HaScanner", FakeHaScanner)
@pytest.mark.usefixtures(
    "macos_adapter", "mock_bleak_scanner_start", "mock_bluetooth_adapters"
)
async def test_diagnostics_macos(
    hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
    """Test diagnostics for macos."""
    # Normally we do not want to patch our classes, but since bleak will import
    # a different scanner based on the operating system, we need to patch here
    # because we cannot import the scanner class directly without it throwing an
    # error if the test is not running on linux since we won't have the correct
    # deps installed when testing on MacOS.
    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"}
    )

    with (
        patch(
            "homeassistant.components.bluetooth.diagnostics.platform.system",
            return_value="Darwin",
        ),
        patch(
            "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects",
            return_value={},
        ),
    ):
        entry1 = MockConfigEntry(
            domain=bluetooth.DOMAIN,
            data={},
            title="Core Bluetooth",
            unique_id=DEFAULT_ADDRESS,
        )
        entry1.add_to_hass(hass)

        assert await hass.config_entries.async_setup(entry1.entry_id)
        await hass.async_block_till_done()

        inject_advertisement(hass, switchbot_device, switchbot_adv)

        diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1)
        assert diag == {
            "adapters": {
                "Core Bluetooth": {
                    "adapter_type": None,
                    "address": "00:00:00:00:00:00",
                    "advertise": True,
                    "manufacturer": "Apple",
                    "passive_scan": False,
                    "powered": True,
                    "product": "Unknown MacOS Model",
                    "product_id": "Unknown",
                    "sw_version": ANY,
                    "vendor_id": "Unknown",
                }
            },
            "manager": {
                "allocations": {
                    "Core Bluetooth": {
                        "allocated": [],
                        "free": 5,
                        "slots": 5,
                        "source": "Core Bluetooth",
                    },
                },
                "adapters": {
                    "Core Bluetooth": {
                        "adapter_type": None,
                        "address": "00:00:00:00:00:00",
                        "advertise": True,
                        "manufacturer": "Apple",
                        "passive_scan": False,
                        "powered": True,
                        "product": "Unknown MacOS Model",
                        "product_id": "Unknown",
                        "sw_version": ANY,
                        "vendor_id": "Unknown",
                    }
                },
                "advertisement_tracker": {
                    "fallback_intervals": {},
                    "intervals": {},
                    "sources": {"44:44:33:11:23:45": "local"},
                    "timings": {"44:44:33:11:23:45": [ANY]},
                },
                "auto_scheduler": {
                    "monotonic_time": ANY,
                    "requests": {},
                    "running": True,
                    "workers": {},
                },
                "all_history": [
                    {
                        "address": "44:44:33:11:23:45",
                        "advertisement": [
                            "wohand",
                            {"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}},
                            {},
                            [],
                            -127,
                            -127,
                            [[]],
                        ],
                        "connectable": True,
                        "device": {
                            "__type": "<class 'bleak.backends.device.BLEDevice'>",
                            "repr": "BLEDevice(44:44:33:11:23:45, wohand)",
                        },
                        "manufacturer_data": {
                            "1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}
                        },
                        "name": "wohand",
                        "raw": None,
                        "rssi": -127,
                        "service_data": {},
                        "service_uuids": [],
                        "source": "local",
                        "time": ANY,
                        "tx_power": -127,
                    }
                ],
                "connectable_history": [
                    {
                        "address": "44:44:33:11:23:45",
                        "advertisement": [
                            "wohand",
                            {"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}},
                            {},
                            [],
                            -127,
                            -127,
                            [[]],
                        ],
                        "connectable": True,
                        "device": {
                            "__type": "<class 'bleak.backends.device.BLEDevice'>",
                            "repr": "BLEDevice(44:44:33:11:23:45, wohand)",
                        },
                        "manufacturer_data": {
                            "1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}
                        },
                        "name": "wohand",
                        "raw": None,
                        "rssi": -127,
                        "service_data": {},
                        "service_uuids": [],
                        "source": "local",
                        "time": ANY,
                        "tx_power": -127,
                    }
                ],
                "scanners": [
                    {
                        "adapter": "Core Bluetooth",
                        "connect_completed_total": 0,
                        "connect_failed_total": 0,
                        "connect_failures": {},
                        "connect_in_progress": {},
                        "connectable": True,
                        "last_connect_completed_time": 0.0,
                        "discovered_devices_and_advertisement_data": [
                            {
                                "address": "44:44:33:11:23:45",
                                "advertisement_data": [
                                    "x",
                                    {},
                                    {},
                                    [],
                                    -127,
                                    -127,
                                    [[]],
                                ],
                                "details": None,
                                "name": "x",
                                "rssi": -127,
                            }
                        ],
                        "last_detection": ANY,
                        "monotonic_time": ANY,
                        "name": "Core Bluetooth",
                        "scanning": True,
                        "source": "Core Bluetooth",
                        "start_time": ANY,
                        "type": "FakeHaScanner",
                        "current_mode": {
                            "__type": "<enum 'BluetoothScanningMode'>",
                            "repr": "<BluetoothScanningMode.ACTIVE: 'active'>",
                        },
                        "requested_mode": {
                            "__type": "<enum 'BluetoothScanningMode'>",
                            "repr": "<BluetoothScanningMode.ACTIVE: 'active'>",
                        },
                    }
                ],
                "slot_manager": {
                    "adapter_slots": {"Core Bluetooth": 5},
                    "allocations_by_adapter": {"Core Bluetooth": []},
                    "manager": False,
                },
            },
        }


@patch("homeassistant.components.bluetooth.HaScanner", FakeHaScanner)
@pytest.mark.usefixtures(
    "enable_bluetooth",
    "one_adapter",
    "mock_bleak_scanner_start",
    "mock_bluetooth_adapters",
)
async def test_diagnostics_remote_adapter(
    hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
    """Test diagnostics for remote adapter."""
    manager = _get_manager()
    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"}
    )

    class FakeScanner(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(),
            )

    with (
        patch(
            "homeassistant.components.bluetooth.diagnostics.platform.system",
            return_value="Linux",
        ),
        patch(
            "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects",
            return_value={},
        ),
    ):
        entry1 = hass.config_entries.async_entries(bluetooth.DOMAIN)[0]
        connector = (
            HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
        )
        scanner = FakeScanner("esp32", "esp32", connector, True)
        unsetup = scanner.async_setup()
        cancel = manager.async_register_scanner(scanner)

        scanner.inject_advertisement(switchbot_device, switchbot_adv)
        inject_advertisement(hass, switchbot_device, switchbot_adv)

        diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1)

        expected = {
            "adapters": {
                "hci0": {
                    "address": "00:00:00:00:00:01",
                    "hw_version": "usb:v1D6Bp0246d053F",
                    "manufacturer": "ACME",
                    "passive_scan": False,
                    "product": "Bluetooth Adapter 5.0",
                    "product_id": "aa01",
                    "sw_version": ANY,
                    "vendor_id": "cc01",
                }
            },
            "dbus": {},
            "manager": {
                "allocations": {
                    "00:00:00:00:00:01": {
                        "allocated": [],
                        "free": 5,
                        "slots": 5,
                        "source": "00:00:00:00:00:01",
                    },
                },
                "adapters": {
                    "hci0": {
                        "address": "00:00:00:00:00:01",
                        "hw_version": "usb:v1D6Bp0246d053F",
                        "manufacturer": "ACME",
                        "passive_scan": False,
                        "product": "Bluetooth Adapter 5.0",
                        "product_id": "aa01",
                        "sw_version": ANY,
                        "vendor_id": "cc01",
                    }
                },
                "advertisement_tracker": {
                    "fallback_intervals": {},
                    "intervals": {},
                    "sources": {"44:44:33:11:23:45": "esp32"},
                    "timings": {"44:44:33:11:23:45": [ANY]},
                },
                "auto_scheduler": {
                    "monotonic_time": ANY,
                    "requests": {},
                    "running": True,
                    "workers": {},
                },
                "all_history": [
                    {
                        "address": "44:44:33:11:23:45",
                        "advertisement": [
                            "wohand",
                            {"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}},
                            {},
                            [],
                            -127,
                            -127,
                            [],
                        ],
                        "connectable": True,
                        "device": {
                            "__type": "<class 'bleak.backends.device.BLEDevice'>",
                            "repr": "BLEDevice(44:44:33:11:23:45, wohand)",
                        },
                        "manufacturer_data": {
                            "1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}
                        },
                        "name": "wohand",
                        "raw": None,
                        "rssi": -127,
                        "service_data": {},
                        "service_uuids": [],
                        "source": "esp32",
                        "time": ANY,
                        "tx_power": -127,
                    }
                ],
                "connectable_history": [
                    {
                        "address": "44:44:33:11:23:45",
                        "advertisement": [
                            "wohand",
                            {"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}},
                            {},
                            [],
                            -127,
                            -127,
                            [],
                        ],
                        "connectable": True,
                        "device": {
                            "__type": "<class 'bleak.backends.device.BLEDevice'>",
                            "repr": "BLEDevice(44:44:33:11:23:45, wohand)",
                        },
                        "manufacturer_data": {
                            "1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}
                        },
                        "name": "wohand",
                        "raw": None,
                        "rssi": -127,
                        "service_data": {},
                        "service_uuids": [],
                        "source": "esp32",
                        "time": ANY,
                        "tx_power": -127,
                    }
                ],
                "scanners": [
                    {
                        "adapter": "hci0",
                        "connect_completed_total": 0,
                        "connect_failed_total": 0,
                        "connect_failures": {},
                        "connect_in_progress": {},
                        "connectable": True,
                        "discovered_devices_and_advertisement_data": [],
                        "last_connect_completed_time": 0.0,
                        "last_detection": ANY,
                        "monotonic_time": ANY,
                        "name": "hci0 (00:00:00:00:00:01)",
                        "scanning": True,
                        "source": "00:00:00:00:00:01",
                        "start_time": ANY,
                        "type": "HaScanner",
                        "current_mode": {
                            "__type": "<enum 'BluetoothScanningMode'>",
                            "repr": "<BluetoothScanningMode.ACTIVE: 'active'>",
                        },
                        "requested_mode": {
                            "__type": "<enum 'BluetoothScanningMode'>",
                            "repr": "<BluetoothScanningMode.ACTIVE: 'active'>",
                        },
                    },
                    {
                        "connect_completed_total": 0,
                        "connect_failed_total": 0,
                        "connect_failures": {},
                        "connect_in_progress": {},
                        "connectable": True,
                        "current_mode": None,
                        "requested_mode": None,
                        "last_connect_completed_time": 0.0,
                        "discovered_device_timestamps": {"44:44:33:11:23:45": ANY},
                        "discovered_devices_and_advertisement_data": [
                            {
                                "address": "44:44:33:11:23:45",
                                "advertisement_data": [
                                    "wohand",
                                    {
                                        "1": {
                                            "__type": "<class 'bytes'>",
                                            "repr": "b'\\x01'",
                                        }
                                    },
                                    {},
                                    [],
                                    -127,
                                    -127,
                                    [],
                                ],
                                "details": {
                                    "scanner_specific_data": "test",
                                    "source": "esp32",
                                },
                                "name": "wohand",
                                "rssi": -127,
                            }
                        ],
                        "last_detection": ANY,
                        "monotonic_time": ANY,
                        "name": "esp32",
                        "scanning": True,
                        "source": "esp32",
                        "start_time": ANY,
                        "time_since_last_device_detection": {"44:44:33:11:23:45": ANY},
                        "raw_advertisement_data": {"44:44:33:11:23:45": None},
                        "type": "FakeScanner",
                    },
                ],
                "slot_manager": {
                    "adapter_slots": {"hci0": 5},
                    "allocations_by_adapter": {"hci0": []},
                    "manager": False,
                },
            },
        }

        diag_scanners = diag["manager"].pop("scanners")
        expected_scanners = expected["manager"].pop("scanners")
        assert diag == expected
        assert sorted(diag_scanners, key=lambda x: x["name"]) == sorted(
            expected_scanners, key=lambda x: x["name"]
        )

    cancel()
    unsetup()
