"""Test Device Tracker config entry things."""

from collections.abc import Generator
from typing import Any

import attr
import pytest

from homeassistant.components.device_tracker import (
    ATTR_HOST_NAME,
    ATTR_IN_ZONES,
    ATTR_IP,
    ATTR_MAC,
    ATTR_SOURCE_TYPE,
    ATTR_TRACKING_TYPE,
    CONF_ASSOCIATED_ZONE,
    CONNECTED_DEVICE_REGISTERED,
    DOMAIN,
    BaseScannerEntity,
    BaseTrackerEntity,
    ScannerEntity,
    SourceType,
    TrackerEntity,
    TrackingType,
)
from homeassistant.components.zone import ATTR_PASSIVE, ATTR_RADIUS
from homeassistant.config_entries import (
    ConfigEntry,
    ConfigEntryState,
    ConfigFlow,
    ConfigSubentryData,
)
from homeassistant.const import (
    ATTR_BATTERY_LEVEL,
    ATTR_FRIENDLY_NAME,
    ATTR_GPS_ACCURACY,
    ATTR_LATITUDE,
    ATTR_LONGITUDE,
    STATE_HOME,
    STATE_NOT_HOME,
    STATE_UNKNOWN,
    Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
    device_registry as dr,
    entity_registry as er,
    issue_registry as ir,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from tests.common import (
    MockConfigEntry,
    MockModule,
    MockPlatform,
    mock_config_flow,
    mock_integration,
    mock_platform,
)

TEST_DOMAIN = "test"
TEST_MAC_ADDRESS = "12:34:56:AB:CD:EF"


class MockFlow(ConfigFlow):
    """Test flow."""


@pytest.fixture(autouse=True)
def config_flow_fixture(hass: HomeAssistant) -> Generator[None]:
    """Mock config flow."""
    mock_platform(hass, f"{TEST_DOMAIN}.config_flow")

    with mock_config_flow(TEST_DOMAIN, MockFlow):
        yield


@pytest.fixture(autouse=True)
def mock_setup_integration(hass: HomeAssistant) -> None:
    """Fixture to set up a mock integration."""

    async def async_setup_entry_init(
        hass: HomeAssistant, config_entry: ConfigEntry
    ) -> bool:
        """Set up test config entry."""
        await hass.config_entries.async_forward_entry_setups(
            config_entry, [Platform.DEVICE_TRACKER]
        )
        return True

    async def async_unload_entry_init(
        hass: HomeAssistant,
        config_entry: ConfigEntry,
    ) -> bool:
        await hass.config_entries.async_unload_platforms(
            config_entry, [Platform.DEVICE_TRACKER]
        )
        return True

    mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
    mock_integration(
        hass,
        MockModule(
            TEST_DOMAIN,
            async_setup_entry=async_setup_entry_init,
            async_unload_entry=async_unload_entry_init,
        ),
    )


@pytest.fixture(name="config_entry")
def config_entry_fixture(hass: HomeAssistant) -> MockConfigEntry:
    """Return the config entry used for the tests."""
    config_entry = MockConfigEntry(domain=TEST_DOMAIN)
    config_entry.add_to_hass(hass)
    return config_entry


async def create_mock_platform(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entities: list[Entity],
    config_subentry_id: str | None = None,
) -> MockConfigEntry:
    """Create a device tracker platform with the specified entities."""

    async def async_setup_entry_platform(
        hass: HomeAssistant,
        config_entry: ConfigEntry,
        async_add_entities: AddConfigEntryEntitiesCallback,
    ) -> None:
        """Set up test event platform via config entry."""
        async_add_entities(entities, config_subentry_id=config_subentry_id)

    mock_platform(
        hass,
        f"{TEST_DOMAIN}.{DOMAIN}",
        MockPlatform(async_setup_entry=async_setup_entry_platform),
    )

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

    return config_entry


@pytest.fixture(name="entity_id")
def entity_id_fixture() -> str:
    """Return the entity_id of the entity for the test."""
    return "device_tracker.entity1"


class MockTrackerEntity(TrackerEntity):
    """Test tracker entity."""

    def __init__(
        self,
        battery_level: int | None = None,
        in_zones: list[str] | None = None,
        location_name: str | None = None,
        latitude: float | None = None,
        longitude: float | None = None,
        location_accuracy: float = 0,
    ) -> None:
        """Initialize entity."""
        self._battery_level = battery_level
        self._in_zones = in_zones
        self._location_name = location_name
        self._latitude = latitude
        self._longitude = longitude
        self._location_accuracy = location_accuracy

    @property
    def battery_level(self) -> int | None:
        """Return the battery level of the device.

        Percentage from 0-100.
        """
        return self._battery_level

    @property
    def source_type(self) -> SourceType:
        """Return the source type, eg gps or router, of the device."""
        return SourceType.GPS

    @property
    def in_zones(self) -> list[str] | None:
        """Return the entity_id of zones the device is currently in."""
        return self._in_zones

    @property
    def location_name(self) -> str | None:
        """Return a location name for the current location of the device."""
        return self._location_name

    @property
    def latitude(self) -> float | None:
        """Return latitude value of the device."""
        return self._latitude

    @property
    def longitude(self) -> float | None:
        """Return longitude value of the device."""
        return self._longitude

    @property
    def location_accuracy(self) -> float:
        """Return the accuracy of the location in meters."""
        return self._location_accuracy


@pytest.fixture(name="battery_level")
def battery_level_fixture() -> int | None:
    """Return the battery level of the entity for the test."""
    return None


@pytest.fixture(name="in_zones")
def in_zones_fixture() -> list[str] | None:
    """Return the in_zones value of the entity for the test."""
    return None


@pytest.fixture(name="location_name")
def location_name_fixture() -> str | None:
    """Return the location_name of the entity for the test."""
    return None


@pytest.fixture(name="latitude")
def latitude_fixture() -> float | None:
    """Return the latitude of the entity for the test."""
    return None


@pytest.fixture(name="longitude")
def longitude_fixture() -> float | None:
    """Return the longitude of the entity for the test."""
    return None


@pytest.fixture(name="location_accuracy")
def accuracy_fixture() -> float:
    """Return the location accuracy of the entity for the test."""
    return 0


@pytest.fixture(name="tracker_entity")
def tracker_entity_fixture(
    entity_id: str,
    battery_level: int | None,
    in_zones: list[str] | None,
    location_name: str | None,
    latitude: float | None,
    longitude: float | None,
    location_accuracy: float = 0,
) -> MockTrackerEntity:
    """Create a test tracker entity."""
    entity = MockTrackerEntity(
        battery_level=battery_level,
        in_zones=in_zones,
        location_name=location_name,
        latitude=latitude,
        longitude=longitude,
        location_accuracy=location_accuracy,
    )
    entity.entity_id = entity_id
    return entity


class MockBaseScannerEntity(BaseScannerEntity):
    """Test base scanner entity."""

    def __init__(
        self,
        connected: bool | None = False,
        unique_id: str | None = None,
    ) -> None:
        """Initialize entity."""
        self._connected = connected
        self._unique_id = unique_id

    @property
    def should_poll(self) -> bool:
        """Return False for the test entity."""
        return False

    @property
    def source_type(self) -> SourceType:
        """Return the source type, eg gps or router, of the device."""
        return SourceType.BLUETOOTH_LE

    @property
    def is_connected(self) -> bool | None:
        """Return true if the device is connected to the network."""
        return self._connected

    @property
    def unique_id(self) -> str | None:
        """Return hostname of the device."""
        return self._unique_id

    @callback
    def set_connected(self, connected: bool | None) -> None:
        """Set connected state."""
        self._connected = connected
        self.async_write_ha_state()


@pytest.fixture(name="unique_id")
def unique_id_fixture() -> str | None:
    """Return the unique_id of the entity for the test."""
    return None


@pytest.fixture(name="base_scanner_entity")
def base_scanner_entity_fixture(
    entity_id: str,
    unique_id: str | None,
) -> MockBaseScannerEntity:
    """Create a test base scanner entity."""
    entity = MockBaseScannerEntity(
        unique_id=unique_id,
    )
    entity.entity_id = entity_id
    return entity


class MockScannerEntity(ScannerEntity):
    """Test scanner entity."""

    def __init__(
        self,
        ip_address: str | None = None,
        mac_address: str | None = None,
        hostname: str | None = None,
        connected: bool | None = False,
        unique_id: str | None = None,
    ) -> None:
        """Initialize entity."""
        self._ip_address = ip_address
        self._mac_address = mac_address
        self._hostname = hostname
        self._connected = connected
        self._unique_id = unique_id

    @property
    def should_poll(self) -> bool:
        """Return False for the test entity."""
        return False

    @property
    def source_type(self) -> SourceType:
        """Return the source type, eg gps or router, of the device."""
        return SourceType.ROUTER

    @property
    def ip_address(self) -> str | None:
        """Return the primary ip address of the device."""
        return self._ip_address

    @property
    def mac_address(self) -> str | None:
        """Return the mac address of the device."""
        return self._mac_address

    @property
    def hostname(self) -> str | None:
        """Return hostname of the device."""
        return self._hostname

    @property
    def is_connected(self) -> bool | None:
        """Return true if the device is connected to the network."""
        return self._connected

    @property
    def unique_id(self) -> str | None:
        """Return hostname of the device."""
        return self._unique_id or self._mac_address

    @callback
    def set_connected(self, connected: bool | None) -> None:
        """Set connected state."""
        self._connected = connected
        self.async_write_ha_state()


@pytest.fixture(name="ip_address")
def ip_address_fixture() -> str | None:
    """Return the ip_address of the entity for the test."""
    return None


@pytest.fixture(name="mac_address")
def mac_address_fixture() -> str | None:
    """Return the mac_address of the entity for the test."""
    return None


@pytest.fixture(name="hostname")
def hostname_fixture() -> str | None:
    """Return the hostname of the entity for the test."""
    return None


@pytest.fixture(name="scanner_entity")
def scanner_entity_fixture(
    entity_id: str,
    ip_address: str | None,
    mac_address: str | None,
    hostname: str | None,
    unique_id: str | None,
) -> MockScannerEntity:
    """Create a test scanner entity."""
    entity = MockScannerEntity(
        ip_address=ip_address,
        mac_address=mac_address,
        hostname=hostname,
        unique_id=unique_id,
    )
    entity.entity_id = entity_id
    return entity


async def test_load_unload_entry_base_scanner(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_id: str,
    base_scanner_entity: MockBaseScannerEntity,
) -> None:
    """Test loading and unloading a config entry with a device tracker entity."""
    config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    state = hass.states.get(entity_id)
    assert state

    assert await hass.config_entries.async_unload(config_entry.entry_id)
    await hass.async_block_till_done()
    assert config_entry.state is ConfigEntryState.NOT_LOADED

    state = hass.states.get(entity_id)
    assert not state


async def test_load_unload_entry_scanner(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_id: str,
    scanner_entity: MockScannerEntity,
) -> None:
    """Test loading and unloading a config entry with a device tracker entity."""
    config_entry = await create_mock_platform(hass, config_entry, [scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    state = hass.states.get(entity_id)
    assert state

    assert await hass.config_entries.async_unload(config_entry.entry_id)
    await hass.async_block_till_done()
    assert config_entry.state is ConfigEntryState.NOT_LOADED

    state = hass.states.get(entity_id)
    assert not state


async def test_load_unload_entry_tracker(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_id: str,
    tracker_entity: MockTrackerEntity,
) -> None:
    """Test loading and unloading a config entry with a device tracker entity."""
    config_entry = await create_mock_platform(hass, config_entry, [tracker_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    state = hass.states.get(entity_id)
    assert state

    assert await hass.config_entries.async_unload(config_entry.entry_id)
    await hass.async_block_till_done()
    assert config_entry.state is ConfigEntryState.NOT_LOADED

    state = hass.states.get(entity_id)
    assert not state


def test_tracking_type_capability_attribute() -> None:
    """Test the tracking_type capability attribute set by each base class."""
    assert MockTrackerEntity().capability_attributes == {
        ATTR_TRACKING_TYPE: TrackingType.POSITION
    }
    assert MockBaseScannerEntity().capability_attributes == {
        ATTR_TRACKING_TYPE: TrackingType.CONNECTION
    }
    assert MockScannerEntity().capability_attributes == {
        ATTR_TRACKING_TYPE: TrackingType.CONNECTION
    }


@pytest.mark.parametrize(
    (
        "battery_level",
        "in_zones",
        "location_name",
        "latitude",
        "longitude",
        "expected_state",
        "expected_attributes",
    ),
    [
        pytest.param(
            None,
            None,
            None,
            1.0,
            2.0,
            STATE_NOT_HOME,
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_GPS_ACCURACY: 0,
                ATTR_IN_ZONES: [],
                ATTR_LATITUDE: 1.0,
                ATTR_LONGITUDE: 2.0,
            },
            id="lat_long_no_zone",
        ),
        pytest.param(
            None,
            None,
            None,
            50.0,
            60.0,
            STATE_HOME,
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_GPS_ACCURACY: 0,
                ATTR_IN_ZONES: ["zone.home"],
                ATTR_LATITUDE: 50.0,
                ATTR_LONGITUDE: 60.0,
            },
            id="lat_long_home",
        ),
        pytest.param(
            None,
            None,
            None,
            -50.0,
            -60.0,
            "other zone",
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_GPS_ACCURACY: 0,
                ATTR_IN_ZONES: ["zone.other_zone", "zone.other_zone_larger"],
                ATTR_LATITUDE: -50.0,
                ATTR_LONGITUDE: -60.0,
            },
            id="lat_long_other_zone",
        ),
        pytest.param(
            None,
            None,
            "zen_zone",
            None,
            None,
            "zen_zone",
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: [],
            },
            id="location_name",
        ),
        pytest.param(
            None,
            None,
            None,
            None,
            None,
            STATE_UNKNOWN,
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: [],
            },
            id="no_location",
        ),
        pytest.param(
            100,
            None,
            None,
            None,
            None,
            STATE_UNKNOWN,
            {
                ATTR_BATTERY_LEVEL: 100,
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: [],
            },
            id="battery_only",
        ),
        pytest.param(
            None,
            ["zone.home"],
            None,
            None,
            None,
            STATE_HOME,
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: ["zone.home"],
            },
            id="in_zones_home",
        ),
        pytest.param(
            None,
            ["zone.other_zone"],
            None,
            None,
            None,
            "other zone",
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: ["zone.other_zone"],
            },
            id="in_zones_other_zone",
        ),
        pytest.param(
            None,
            ["zone.other_zone_larger", "zone.other_zone"],
            None,
            None,
            None,
            "other zone",
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: ["zone.other_zone", "zone.other_zone_larger"],
            },
            id="in_zones_multiple_sorted_by_radius",
        ),
        pytest.param(
            None,
            ["zone.does_not_exist", "zone.other_zone"],
            None,
            None,
            None,
            "other zone",
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: ["zone.other_zone"],
            },
            id="in_zones_filters_missing_zones",
        ),
        pytest.param(
            None,
            ["zone.does_not_exist"],
            None,
            None,
            None,
            STATE_NOT_HOME,
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: [],
            },
            id="in_zones_all_missing",
        ),
        pytest.param(
            None,
            ["zone.passive_small", "zone.other_zone"],
            None,
            None,
            None,
            "other zone",
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: ["zone.passive_small", "zone.other_zone"],
            },
            id="in_zones_skips_passive_for_state",
        ),
        pytest.param(
            None,
            ["zone.passive_small"],
            None,
            None,
            None,
            STATE_NOT_HOME,
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: ["zone.passive_small"],
            },
            id="in_zones_only_passive",
        ),
        pytest.param(
            None,
            [],
            None,
            None,
            None,
            STATE_NOT_HOME,
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: [],
            },
            id="in_zones_empty",
        ),
        pytest.param(
            None,
            ["zone.home"],
            None,
            1.0,
            2.0,
            STATE_HOME,
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_GPS_ACCURACY: 0,
                ATTR_IN_ZONES: ["zone.home"],
                ATTR_LATITUDE: 1.0,
                ATTR_LONGITUDE: 2.0,
            },
            id="in_zones_wins_over_lat_long",
        ),
        pytest.param(
            None,
            [],
            None,
            50.0,
            60.0,
            STATE_NOT_HOME,
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_GPS_ACCURACY: 0,
                ATTR_IN_ZONES: [],
                ATTR_LATITUDE: 50.0,
                ATTR_LONGITUDE: 60.0,
            },
            id="empty_in_zones_wins_over_lat_long",
        ),
        pytest.param(
            None,
            ["zone.home"],
            "zen_zone",
            None,
            None,
            "zen_zone",
            {
                ATTR_SOURCE_TYPE: SourceType.GPS,
                ATTR_IN_ZONES: ["zone.home"],
            },
            id="location_name_wins_over_in_zones",
        ),
    ],
)
async def test_tracker_entity_state(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_id: str,
    tracker_entity: MockTrackerEntity,
    expected_state: str,
    expected_attributes: dict[str, Any],
) -> None:
    """Test tracker entity state and state attributes."""
    config_entry = await create_mock_platform(hass, config_entry, [tracker_entity])
    assert config_entry.state is ConfigEntryState.LOADED
    hass.states.async_set(
        "zone.home",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 200},
    )
    hass.states.async_set(
        "zone.other_zone",
        "0",
        {ATTR_LATITUDE: -50.0, ATTR_LONGITUDE: -60.0, ATTR_RADIUS: 300},
    )
    hass.states.async_set(
        "zone.other_zone_larger",
        "0",
        {ATTR_LATITUDE: -50.0, ATTR_LONGITUDE: -60.0, ATTR_RADIUS: 500},
    )
    hass.states.async_set(
        "zone.passive_small",
        "0",
        {
            ATTR_LATITUDE: 10.0,
            ATTR_LONGITUDE: 10.0,
            ATTR_RADIUS: 50,
            ATTR_PASSIVE: True,
        },
    )
    await hass.async_block_till_done()
    # Write state again to ensure the zone state is taken into account.
    tracker_entity.async_write_ha_state()

    state = hass.states.get(entity_id)
    assert state
    assert state.state == expected_state
    assert state.attributes == expected_attributes | {
        ATTR_TRACKING_TYPE: TrackingType.POSITION
    }


async def test_base_scanner_entity_state(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_id: str,
    base_scanner_entity: MockBaseScannerEntity,
) -> None:
    """Test BaseScannerEntity based device tracker."""
    config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.attributes == {
        ATTR_SOURCE_TYPE: SourceType.BLUETOOTH_LE,
        ATTR_TRACKING_TYPE: TrackingType.CONNECTION,
        ATTR_IN_ZONES: [],
    }
    assert entity_state.state == STATE_NOT_HOME

    base_scanner_entity.set_connected(True)
    await hass.async_block_till_done()

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == STATE_HOME
    # No zone.home in the test state machine, so only the canonical home
    # entity_id is reported.
    assert entity_state.attributes == {
        ATTR_SOURCE_TYPE: SourceType.BLUETOOTH_LE,
        ATTR_TRACKING_TYPE: TrackingType.CONNECTION,
        ATTR_IN_ZONES: ["zone.home"],
    }

    base_scanner_entity.set_connected(None)
    await hass.async_block_till_done()

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == STATE_UNKNOWN
    # is_connected is None -> empty in_zones (always reported).
    assert entity_state.attributes == {
        ATTR_SOURCE_TYPE: SourceType.BLUETOOTH_LE,
        ATTR_TRACKING_TYPE: TrackingType.CONNECTION,
        ATTR_IN_ZONES: [],
    }


@pytest.mark.parametrize(
    ("zones", "expected_in_zones"),
    [
        pytest.param(
            [("zone.home", 50.0, 60.0, 100)],
            ["zone.home"],
            id="home_only",
        ),
        pytest.param(
            [
                ("zone.home", 50.0, 60.0, 100),
                ("zone.neighborhood", 50.0, 60.0, 500),
            ],
            ["zone.home", "zone.neighborhood"],
            id="strictly_containing_zone",
        ),
        pytest.param(
            [
                ("zone.home", 50.0, 60.0, 100),
                ("zone.huge", 50.0, 60.0, 10000),
                ("zone.medium", 50.0, 60.0, 500),
            ],
            ["zone.home", "zone.medium", "zone.huge"],
            id="multiple_containing_zones_sorted_by_radius",
        ),
        pytest.param(
            [
                ("zone.home", 50.0, 60.0, 100),
                ("zone.tiny", 50.0, 60.0, 50),
            ],
            ["zone.home"],
            id="zone_smaller_than_home_excluded",
        ),
        pytest.param(
            [
                ("zone.home", 50.0, 60.0, 100),
                ("zone.equal", 50.0, 60.0, 100),
            ],
            # Same center and radius as home: included under the <= predicate.
            # zone.home stays first because the strict-result zone.home entry
            # is filtered out, and zone.equal is the next entry.
            ["zone.home", "zone.equal"],
            id="zone_equal_to_home_included",
        ),
        pytest.param(
            [
                ("zone.home", 50.0, 60.0, 100),
                # Small offset, the home zone is fully inside
                # the other zone (~330m + 100 < 500).
                ("zone.nearby", 50.0030, 60.0, 500),
                # Offset by enough that the home zone is not fully inside
                # the other zone (~440m + 100 > 500).
                ("zone.further_away", 50.0040, 60.0, 500),
                # Offset by a very large amount, no overlap
                # the other zone (~130km + 100 > 500).
                ("zone.faraway", 51.0, 61.0, 500),
            ],
            ["zone.home", "zone.nearby"],
            id="offset_zone_excluded",
        ),
    ],
)
async def test_base_scanner_entity_in_zones_when_connected(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_id: str,
    base_scanner_entity: MockBaseScannerEntity,
    zones: list[tuple[str, float, float, int]],
    expected_in_zones: list[str],
) -> None:
    """Test in_zones content for a connected BaseScannerEntity across zone setups."""
    base_scanner_entity._connected = True

    for entity, latitude, longitude, radius in zones:
        hass.states.async_set(
            entity,
            "0",
            {ATTR_LATITUDE: latitude, ATTR_LONGITUDE: longitude, ATTR_RADIUS: radius},
        )
    await hass.async_block_till_done()

    config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == STATE_HOME
    assert entity_state.attributes == {
        ATTR_SOURCE_TYPE: SourceType.BLUETOOTH_LE,
        ATTR_TRACKING_TYPE: TrackingType.CONNECTION,
        ATTR_IN_ZONES: expected_in_zones,
    }


@pytest.mark.parametrize("unique_id", ["unique_scanner"])
async def test_base_scanner_entity_associated_zone_option(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    entity_id: str,
    base_scanner_entity: MockBaseScannerEntity,
) -> None:
    """Test the associated_zone entity option overrides which zone in_zones reports.

    The scanner reports being connected to a non-default zone; state and in_zones
    must follow the configured zone, and a zone enclosing the configured one is
    included in in_zones too.
    """
    hass.states.async_set(
        "zone.home",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
    )
    hass.states.async_set(
        "zone.kitchen",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
    )
    await hass.async_block_till_done()

    base_scanner_entity._connected = True

    config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    # Default: no option set -> associated with zone.home.
    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == STATE_HOME
    assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.home"]

    # Set the option -> associated_zone replaces zone.home; zone.home now shows
    # up via the enclosing-zones lookup.
    entity_registry.async_update_entity_options(
        entity_id,
        DOMAIN,
        {CONF_ASSOCIATED_ZONE: "zone.kitchen"},
    )
    await hass.async_block_till_done()

    assert base_scanner_entity._scanner_option_associated_zone == "zone.kitchen"

    entity_state = hass.states.get(entity_id)
    assert entity_state
    # zone.kitchen is the configured zone -> state is the zone's name.
    assert entity_state.state == "kitchen"
    assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]

    # Clearing the option falls back to zone.home.
    entity_registry.async_update_entity_options(entity_id, DOMAIN, None)
    await hass.async_block_till_done()

    assert base_scanner_entity._scanner_option_associated_zone == "zone.home"

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == STATE_HOME
    assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.home"]


@pytest.mark.parametrize("unique_id", ["unique_scanner"])
async def test_base_scanner_entity_associated_zone_removed_after_set(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    issue_registry: ir.IssueRegistry,
    entity_id: str,
    base_scanner_entity: MockBaseScannerEntity,
) -> None:
    """Test scanner state and repair issue when associated zone is removed.

    When the user picks a zone via the associated_zone option and then deletes
    that zone, the scanner falls back to ``state == "unknown"`` and a repair
    issue is opened prompting the user to reconfigure.
    """
    hass.states.async_set(
        "zone.home",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
    )
    hass.states.async_set(
        "zone.kitchen",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
    )
    await hass.async_block_till_done()

    base_scanner_entity._connected = True

    config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    entity_registry.async_update_entity_options(
        entity_id,
        DOMAIN,
        {CONF_ASSOCIATED_ZONE: "zone.kitchen"},
    )
    await hass.async_block_till_done()

    # Sanity check before removal.
    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == "kitchen"
    assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]
    entity_entry = entity_registry.async_get(entity_id)
    assert entity_entry
    issue_id = f"associated_zone_missing_{entity_entry.id}"
    assert issue_registry.async_get_issue(DOMAIN, issue_id) is None

    # Remove the associated zone.
    hass.states.async_remove("zone.kitchen")
    await hass.async_block_till_done()

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == STATE_UNKNOWN
    assert entity_state.attributes[ATTR_IN_ZONES] == []
    issue = issue_registry.async_get_issue(DOMAIN, issue_id)
    assert issue is not None
    assert issue.severity is ir.IssueSeverity.WARNING
    assert issue.translation_key == "associated_zone_missing"
    assert issue.translation_placeholders == {
        "entity_id": entity_id,
        "zone": "zone.kitchen",
    }

    # Restore the zone -> issue is cleared, state recovers.
    hass.states.async_set(
        "zone.kitchen",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
    )
    await hass.async_block_till_done()

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == "kitchen"
    assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]
    assert issue_registry.async_get_issue(DOMAIN, issue_id) is None


@pytest.mark.parametrize("unique_id", ["unique_scanner"])
async def test_base_scanner_entity_associated_zone_missing_at_setup(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    issue_registry: ir.IssueRegistry,
    entity_id: str,
    base_scanner_entity: MockBaseScannerEntity,
) -> None:
    """Test repair issue is created when the configured zone is missing at setup."""
    hass.states.async_set(
        "zone.home",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
    )
    await hass.async_block_till_done()

    # Pre-register the entity option pointing at a zone that does not exist.
    entity_entry = entity_registry.async_get_or_create(
        DOMAIN,
        TEST_DOMAIN,
        base_scanner_entity.unique_id,
        suggested_object_id="entity1",
    )
    entity_registry.async_update_entity_options(
        entity_id,
        DOMAIN,
        {CONF_ASSOCIATED_ZONE: "zone.never_existed"},
    )

    base_scanner_entity._connected = True
    config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == STATE_UNKNOWN
    assert entity_state.attributes[ATTR_IN_ZONES] == []
    issue_id = f"associated_zone_missing_{entity_entry.id}"
    issue = issue_registry.async_get_issue(DOMAIN, issue_id)
    assert issue is not None
    assert issue.translation_placeholders == {
        "entity_id": entity_id,
        "zone": "zone.never_existed",
    }


@pytest.mark.parametrize("unique_id", ["unique_scanner"])
async def test_base_scanner_entity_associated_zone_issue_cleared_on_option_change(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    issue_registry: ir.IssueRegistry,
    entity_id: str,
    base_scanner_entity: MockBaseScannerEntity,
) -> None:
    """Test the repair issue is cleared when the user clears the option."""
    hass.states.async_set(
        "zone.home",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
    )
    await hass.async_block_till_done()

    base_scanner_entity._connected = True
    config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    entity_registry.async_update_entity_options(
        entity_id,
        DOMAIN,
        {CONF_ASSOCIATED_ZONE: "zone.never_existed"},
    )
    await hass.async_block_till_done()

    entity_entry = entity_registry.async_get(entity_id)
    assert entity_entry
    issue_id = f"associated_zone_missing_{entity_entry.id}"
    assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None

    # Clearing the option restores the default and clears the repair issue.
    entity_registry.async_update_entity_options(entity_id, DOMAIN, None)
    await hass.async_block_till_done()

    assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == STATE_HOME


@pytest.mark.parametrize("unique_id", ["unique_scanner"])
async def test_base_scanner_entity_associated_zone_issue_cleared_on_unload(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    issue_registry: ir.IssueRegistry,
    entity_id: str,
    base_scanner_entity: MockBaseScannerEntity,
) -> None:
    """Test the repair issue is cleared when the entity is removed from hass."""
    hass.states.async_set(
        "zone.home",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
    )
    await hass.async_block_till_done()

    base_scanner_entity._connected = True
    config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    entity_registry.async_update_entity_options(
        entity_id,
        DOMAIN,
        {CONF_ASSOCIATED_ZONE: "zone.never_existed"},
    )
    await hass.async_block_till_done()

    entity_entry = entity_registry.async_get(entity_id)
    assert entity_entry
    issue_id = f"associated_zone_missing_{entity_entry.id}"
    assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None

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

    assert issue_registry.async_get_issue(DOMAIN, issue_id) is None


@pytest.mark.parametrize("unique_id", ["unique_scanner"])
async def test_base_scanner_entity_associated_zone_option_set_before_add(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    entity_id: str,
    base_scanner_entity: MockBaseScannerEntity,
) -> None:
    """Test associated_zone option set before the entity is added is honored."""
    hass.states.async_set(
        "zone.home",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
    )
    hass.states.async_set(
        "zone.kitchen",
        "0",
        {ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
    )
    await hass.async_block_till_done()

    # Pre-register the entity with the option set before the platform is set up.
    entity_registry.async_get_or_create(
        DOMAIN,
        TEST_DOMAIN,
        base_scanner_entity.unique_id,
        suggested_object_id="entity1",
    )
    entity_registry.async_update_entity_options(
        entity_id,
        DOMAIN,
        {CONF_ASSOCIATED_ZONE: "zone.kitchen"},
    )

    base_scanner_entity._connected = True
    config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    assert base_scanner_entity._scanner_option_associated_zone == "zone.kitchen"

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == "kitchen"
    assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]


@pytest.mark.parametrize(
    ("ip_address", "mac_address", "hostname"),
    [("0.0.0.0", "ad:de:ef:be:ed:fe", "test.hostname.org")],
)
async def test_scanner_entity_state(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
    entity_id: str,
    ip_address: str,
    mac_address: str,
    hostname: str,
    scanner_entity: MockScannerEntity,
) -> None:
    """Test ScannerEntity based device tracker."""
    # Make device tied to other integration so device tracker entities get enabled
    other_config_entry = MockConfigEntry(domain="not_fake_integration")
    other_config_entry.add_to_hass(hass)
    device_registry.async_get_or_create(
        name="Device from other integration",
        config_entry_id=other_config_entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, mac_address)},
    )

    config_entry = await create_mock_platform(hass, config_entry, [scanner_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.attributes == {
        ATTR_SOURCE_TYPE: SourceType.ROUTER,
        ATTR_TRACKING_TYPE: TrackingType.CONNECTION,
        ATTR_IN_ZONES: [],
        ATTR_IP: ip_address,
        ATTR_MAC: mac_address,
        ATTR_HOST_NAME: hostname,
        ATTR_FRIENDLY_NAME: hostname,
    }
    assert entity_state.state == STATE_NOT_HOME

    scanner_entity.set_connected(True)
    await hass.async_block_till_done()

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == STATE_HOME

    scanner_entity.set_connected(None)
    await hass.async_block_till_done()

    entity_state = hass.states.get(entity_id)
    assert entity_state
    assert entity_state.state == STATE_UNKNOWN


def test_tracker_entity() -> None:
    """Test coverage for base TrackerEntity class."""
    entity = TrackerEntity()
    assert entity.source_type is SourceType.GPS
    assert entity.in_zones is None
    assert entity.latitude is None
    assert entity.longitude is None
    assert entity.location_name is None
    assert entity.state is None
    assert entity.battery_level is None
    assert entity.should_poll is False
    assert entity.force_update is True
    assert entity.location_accuracy == 0

    class MockEntity(TrackerEntity):
        """Mock tracker class."""

        def __init__(self) -> None:
            """Initialize."""
            self.is_polling = False

        @property
        def should_poll(self) -> bool:
            """Return False for the test entity."""
            return self.is_polling

    test_entity = MockEntity()

    assert test_entity.force_update

    test_entity.is_polling = True

    assert not test_entity.force_update


def test_base_scanner_entity() -> None:
    """Test coverage for base BaseScannerEntity entity class."""
    entity = BaseScannerEntity()
    with pytest.raises(NotImplementedError):
        entity.source_type  # noqa: B018
    with pytest.raises(NotImplementedError):
        entity.is_connected  # noqa: B018
    with pytest.raises(NotImplementedError):
        entity.state  # noqa: B018
    assert entity.battery_level is None


def test_scanner_entity() -> None:
    """Test coverage for base ScannerEntity entity class."""
    entity = ScannerEntity()
    assert entity.source_type is SourceType.ROUTER
    with pytest.raises(NotImplementedError):
        entity.is_connected  # noqa: B018
    with pytest.raises(NotImplementedError):
        entity.state  # noqa: B018
    assert entity.battery_level is None
    assert entity.ip_address is None
    assert entity.mac_address is None
    assert entity.hostname is None

    class MockEntity(ScannerEntity):
        """Mock scanner class."""

        def __init__(self) -> None:
            """Initialize."""
            self.mock_mac_address: str | None = None

        @property
        def mac_address(self) -> str | None:
            """Return the mac address of the device."""
            return self.mock_mac_address

    test_entity = MockEntity()

    assert test_entity.unique_id is None

    test_entity.mock_mac_address = TEST_MAC_ADDRESS

    assert test_entity.unique_id == TEST_MAC_ADDRESS


def test_base_tracker_entity() -> None:
    """Test coverage for base BaseTrackerEntity entity class."""
    entity = BaseTrackerEntity()
    with pytest.raises(NotImplementedError):
        entity.source_type  # noqa: B018
    assert entity.battery_level is None
    with pytest.raises(NotImplementedError):
        entity.state_attributes  # noqa: B018


def test_battery_level_override_deprecation_warning(
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test that overriding battery_level in a subclass logs a warning."""
    error_message = "is overriding the deprecated battery_level property"

    caplog.clear()

    class _SubclassWithOverride(TrackerEntity):
        @property
        def battery_level(self) -> int | None:
            return 50

    assert error_message in caplog.text
    assert _SubclassWithOverride.__name__ in caplog.text

    # No warning for a subclass that does not override battery_level
    caplog.clear()

    class _SubclassWithoutOverride(TrackerEntity):
        pass

    assert error_message not in caplog.text


async def test_attr_location_name_deprecation_warning(
    hass: HomeAssistant,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test that setting _attr_location_name logs a deprecation warning."""
    error_message = "is setting the deprecated _attr_location_name attribute"

    class _Subclass(TrackerEntity):
        pass

    # No warning when _attr_location_name is unset (default None)
    entity_no_attr = _Subclass()
    entity_no_attr.hass = hass
    assert entity_no_attr.location_name is None
    assert error_message not in caplog.text

    # Warning fires when _attr_location_name has a non-None value
    entity = _Subclass()
    entity.hass = hass
    entity._attr_location_name = "the_zone"
    caplog.clear()
    assert entity.location_name == "the_zone"
    assert error_message in caplog.text

    # Warning does not fire again on subsequent access for the same instance
    caplog.clear()
    assert entity.location_name == "the_zone"
    assert error_message not in caplog.text

    # Warning is suppressed for this instance even after the cached value is
    # invalidated by a subsequent _attr_location_name assignment.
    entity._attr_location_name = "another_zone"
    caplog.clear()
    assert entity.location_name == "another_zone"
    assert error_message not in caplog.text

    # A fresh instance warns once again
    entity_new = _Subclass()
    entity_new.hass = hass
    entity_new._attr_location_name = "the_zone"
    caplog.clear()
    assert entity_new.location_name == "the_zone"
    assert error_message in caplog.text


def test_location_name_override_deprecation_warning(
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test that overriding location_name in a subclass logs a warning."""
    error_message = "is overriding the deprecated location_name property"

    caplog.clear()

    class _SubclassWithOverride(TrackerEntity):
        @property
        def location_name(self) -> str | None:
            return "custom"

    assert error_message in caplog.text
    assert _SubclassWithOverride.__name__ in caplog.text

    # No warning for a subclass that does not override location_name
    caplog.clear()

    class _SubclassWithoutOverride(TrackerEntity):
        pass

    assert error_message not in caplog.text


@pytest.mark.parametrize(
    ("mac_address", "unique_id"), [(TEST_MAC_ADDRESS, f"{TEST_MAC_ADDRESS}_yo1")]
)
async def test_register_mac(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
    scanner_entity: MockScannerEntity,
    entity_id: str,
    mac_address: str,
    unique_id: str,
) -> None:
    """Test registering a mac."""
    await create_mock_platform(hass, config_entry, [scanner_entity])

    entity_entry = entity_registry.async_get(entity_id)
    assert entity_entry is not None
    assert entity_entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION

    device_registry.async_get_or_create(
        config_entry_id=config_entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, mac_address)},
    )
    await hass.async_block_till_done()

    entity_entry = entity_registry.async_get(entity_id)
    assert entity_entry is not None
    assert entity_entry.disabled_by is None


@pytest.mark.parametrize(
    ("connections", "mac_address", "unique_id"),
    [
        (
            set(),
            TEST_MAC_ADDRESS,
            f"{TEST_MAC_ADDRESS}_yo1",
        ),
        (
            {(dr.CONNECTION_NETWORK_MAC, TEST_MAC_ADDRESS)},
            "aa:bb:cc:dd:ee:ff",
            "aa_bb_cc_dd_ee_ff",
        ),
    ],
)
async def test_register_mac_not_found(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
    scanner_entity: MockScannerEntity,
    entity_id: str,
    connections: set[tuple[str, str]],
    mac_address: str,
    unique_id: str,
) -> None:
    """Test registering a mac when the mac or entity isn't found."""
    registering_scanner_entity = MockScannerEntity(mac_address="aa:bb:cc:dd:ee:ff")
    registering_scanner_entity.entity_id = f"{DOMAIN}.registering_scanner_entity"

    await create_mock_platform(
        hass, config_entry, [registering_scanner_entity, scanner_entity]
    )

    test_entity_entry = entity_registry.async_get(entity_id)
    assert test_entity_entry is not None
    assert test_entity_entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION

    device_registry.async_get_or_create(
        config_entry_id=config_entry.entry_id,
        connections=connections,
        identifiers={(TEST_DOMAIN, "device1")},
    )
    await hass.async_block_till_done()

    # The entity entry under test should still be disabled.
    test_entity_entry = entity_registry.async_get(entity_id)
    assert test_entity_entry is not None
    assert test_entity_entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION


@pytest.mark.parametrize(
    ("mac_address", "unique_id"), [(TEST_MAC_ADDRESS, f"{TEST_MAC_ADDRESS}_yo1")]
)
async def test_register_mac_ignored(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
    scanner_entity: MockScannerEntity,
    entity_id: str,
    mac_address: str,
    unique_id: str,
) -> None:
    """Test ignoring registering a mac."""
    config_entry = MockConfigEntry(domain=TEST_DOMAIN, pref_disable_new_entities=True)
    config_entry.add_to_hass(hass)

    await create_mock_platform(hass, config_entry, [scanner_entity])

    entity_entry = entity_registry.async_get(entity_id)
    assert entity_entry is not None
    assert entity_entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION

    device_registry.async_get_or_create(
        config_entry_id=config_entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, mac_address)},
    )
    await hass.async_block_till_done()

    entity_entry = entity_registry.async_get(entity_id)
    assert entity_entry is not None
    assert entity_entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION


async def test_scanner_entity_attaches_to_split_of_composite_device(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test that a scanner entity attaches to its config entry's split device."""
    mac = TEST_MAC_ADDRESS
    other_entry = MockConfigEntry(domain="other")
    other_entry.add_to_hass(hass)
    old_id = "composite00000000000000000000000"
    own_split = device_registry.async_get_or_create(
        config_entry_id=config_entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, mac)},
        identifiers={(TEST_DOMAIN, "own")},
    )
    other_split = device_registry.async_get_or_create(
        config_entry_id=other_entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, mac)},
        identifiers={("other", "x")},
    )
    # Simulate a migration split: both devices share the pre-migration composite id
    device_registry.devices[own_split.id] = attr.evolve(
        own_split, composite_device_id=old_id
    )
    device_registry.devices[other_split.id] = attr.evolve(
        other_split, composite_device_id=old_id
    )
    # async_get_device now resolves the shared MAC to the synthesized composite
    composite = device_registry.async_get_device(
        connections={(dr.CONNECTION_NETWORK_MAC, mac)}
    )
    assert composite is not None
    assert composite.id == old_id
    assert old_id not in device_registry.devices

    scanner_entity = MockScannerEntity(mac_address=mac, unique_id=f"{mac}_scanner")
    scanner_entity.entity_id = "device_tracker.composite_scanner"
    await create_mock_platform(hass, config_entry, [scanner_entity])

    # Attached to its own split, not the un-assignable composite id
    entity_entry = entity_registry.async_get("device_tracker.composite_scanner")
    assert entity_entry is not None
    assert entity_entry.device_id == own_split.id


async def test_scanner_entity_composite_device_without_own_split(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
) -> None:
    """A composite with no split owned by the scanner's config entry.

    The tracked device is known to other integrations, so the scanner registers
    its own device instead of attaching to another config entry's split.
    """
    mac = TEST_MAC_ADDRESS
    other_entry_1 = MockConfigEntry(domain="other_1")
    other_entry_1.add_to_hass(hass)
    other_entry_2 = MockConfigEntry(domain="other_2")
    other_entry_2.add_to_hass(hass)
    old_id = "composite00000000000000000000000"
    # Both splits belong to other config entries, none to the scanner's
    for entry, identifier in ((other_entry_1, "one"), (other_entry_2, "two")):
        split = device_registry.async_get_or_create(
            config_entry_id=entry.entry_id,
            connections={(dr.CONNECTION_NETWORK_MAC, mac)},
            identifiers={("other", identifier)},
        )
        device_registry.devices[split.id] = attr.evolve(
            split, composite_device_id=old_id
        )
    composite = device_registry.async_get_device(
        connections={(dr.CONNECTION_NETWORK_MAC, mac)}
    )
    assert composite is not None
    assert composite.id == old_id
    assert old_id not in device_registry.devices

    scanner_entity = MockScannerEntity(mac_address=mac, unique_id=f"{mac}_scanner")
    scanner_entity.entity_id = "device_tracker.composite_scanner"
    await create_mock_platform(hass, config_entry, [scanner_entity])

    # The scanner registers its own device instead of using a foreign split
    entity_entry = entity_registry.async_get("device_tracker.composite_scanner")
    assert entity_entry is not None
    assert entity_entry.device_id is not None
    own_device = device_registry.async_get(entity_entry.device_id)
    assert own_device is not None
    assert own_device.config_entry_id == config_entry.entry_id


async def test_scanner_entity_attaches_to_own_device(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test the scanner reuses its config entry's device when several share the MAC."""
    mac = TEST_MAC_ADDRESS
    other_entry = MockConfigEntry(domain="other")
    other_entry.add_to_hass(hass)
    device_registry.async_get_or_create(
        config_entry_id=other_entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, mac)},
        identifiers={("other", "x")},
    )
    own_device = device_registry.async_get_or_create(
        config_entry_id=config_entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, mac)},
    )

    scanner_entity = MockScannerEntity(mac_address=mac, unique_id=f"{mac}_scanner")
    scanner_entity.entity_id = "device_tracker.shared_mac_scanner"
    await create_mock_platform(hass, config_entry, [scanner_entity])

    entity_entry = entity_registry.async_get("device_tracker.shared_mac_scanner")
    assert entity_entry is not None
    assert entity_entry.device_id == own_device.id


@pytest.mark.parametrize(
    ("hostname", "expected_device_name"),
    [
        pytest.param("tracked-host", "tracked-host", id="hostname"),
        pytest.param(None, TEST_MAC_ADDRESS, id="no-hostname-falls-back-to-mac"),
    ],
)
async def test_scanner_entity_registers_own_device(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
    hostname: str | None,
    expected_device_name: str,
) -> None:
    """Test the scanner registers its own device when the MAC is known elsewhere."""
    mac = TEST_MAC_ADDRESS
    foreign_ids = set()
    for domain in ("other_1", "other_2"):
        entry = MockConfigEntry(domain=domain)
        entry.add_to_hass(hass)
        foreign_ids.add(
            device_registry.async_get_or_create(
                config_entry_id=entry.entry_id,
                connections={(dr.CONNECTION_NETWORK_MAC, mac)},
                identifiers={(domain, "x")},
            ).id
        )

    scanner_entity = MockScannerEntity(
        mac_address=mac, hostname=hostname, unique_id=f"{mac}_scanner"
    )
    scanner_entity.entity_id = "device_tracker.shared_mac_scanner"
    await create_mock_platform(hass, config_entry, [scanner_entity])

    entity_entry = entity_registry.async_get("device_tracker.shared_mac_scanner")
    assert entity_entry is not None
    assert entity_entry.device_id is not None
    assert entity_entry.device_id not in foreign_ids
    own_device = device_registry.async_get(entity_entry.device_id)
    assert own_device is not None
    assert own_device.config_entry_id == config_entry.entry_id
    assert own_device.connections == {(dr.CONNECTION_NETWORK_MAC, dr.format_mac(mac))}
    assert own_device.name == expected_device_name


async def test_scanner_entity_own_device_in_subentry(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test the scanner's own device is created in the entity's config subentry."""
    mac = TEST_MAC_ADDRESS
    subentry_id = "mock-subentry-id"
    config_entry = MockConfigEntry(
        domain=TEST_DOMAIN,
        subentries_data=(
            ConfigSubentryData(
                data={},
                subentry_id=subentry_id,
                subentry_type="test",
                title="Mock subentry",
                unique_id=None,
            ),
        ),
    )
    config_entry.add_to_hass(hass)
    other_entry = MockConfigEntry(domain="other")
    other_entry.add_to_hass(hass)
    device_registry.async_get_or_create(
        config_entry_id=other_entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, mac)},
        identifiers={("other", "x")},
    )

    scanner_entity = MockScannerEntity(mac_address=mac, unique_id=f"{mac}_scanner")
    scanner_entity.entity_id = "device_tracker.subentry_scanner"
    await create_mock_platform(
        hass, config_entry, [scanner_entity], config_subentry_id=subentry_id
    )

    entity_entry = entity_registry.async_get("device_tracker.subentry_scanner")
    assert entity_entry is not None
    assert entity_entry.config_subentry_id == subentry_id
    assert entity_entry.device_id is not None
    own_device = device_registry.async_get(entity_entry.device_id)
    assert own_device is not None
    assert own_device.config_entry_id == config_entry.entry_id
    assert own_device.config_subentry_id == subentry_id


async def test_scanner_entity_prunes_composite_identifiers(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test re-registration prunes composite relics from the scanner's split device.

    Splits of a pre-migration composite keep the identifiers and connections copied
    from the composite until the owning config entry re-registers the device; the
    scanner's registration provides only the MAC connection, dropping the rest.
    """
    mac = TEST_MAC_ADDRESS
    own_split = device_registry.async_get_or_create(
        config_entry_id=config_entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, mac)},
        identifiers={("other", "copied-identifier")},
    )
    device_registry.devices[own_split.id] = attr.evolve(
        own_split,
        composite_device_id="composite00000000000000000000000",
        has_composite_identifiers=True,
    )

    scanner_entity = MockScannerEntity(mac_address=mac, unique_id=f"{mac}_scanner")
    scanner_entity.entity_id = "device_tracker.composite_scanner"
    await create_mock_platform(hass, config_entry, [scanner_entity])

    entity_entry = entity_registry.async_get("device_tracker.composite_scanner")
    assert entity_entry is not None
    assert entity_entry.device_id == own_split.id
    device = device_registry.async_get(own_split.id)
    assert device is not None
    assert device.identifiers == set()
    assert device.connections == {(dr.CONNECTION_NETWORK_MAC, dr.format_mac(mac))}


async def test_connected_device_registered(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test dispatch on connected device being registered."""
    dispatches: list[dict[str, Any]] = []

    @callback
    def _save_dispatch(msg: dict[str, Any]) -> None:
        """Save dispatched message."""
        dispatches.append(msg)

    unsub = async_dispatcher_connect(hass, CONNECTED_DEVICE_REGISTERED, _save_dispatch)

    connected_scanner_entity = MockScannerEntity(
        ip_address="5.4.3.2",
        mac_address="aa:bb:cc:dd:ee:ff",
        hostname="connected",
        connected=True,
    )
    disconnected_scanner_entity = MockScannerEntity(
        ip_address="5.4.3.2",
        mac_address="aa:bb:cc:dd:ee:00",
        hostname="disconnected",
        connected=False,
    )
    connected_scanner_entity_bad_ip = MockScannerEntity(
        ip_address="",
        mac_address="aa:bb:cc:dd:ee:01",
        hostname="connected_bad_ip",
        connected=True,
    )

    config_entry = await create_mock_platform(
        hass,
        config_entry,
        [
            connected_scanner_entity,
            disconnected_scanner_entity,
            connected_scanner_entity_bad_ip,
        ],
    )

    full_name = f"{config_entry.domain}.{DOMAIN}"
    assert full_name in hass.config.components
    assert (
        len(hass.states.async_entity_ids(domain_filter=DOMAIN)) == 0
    )  # should be disabled
    assert len(entity_registry.entities) == 3
    assert (
        entity_registry.entities[
            "device_tracker.test_aa_bb_cc_dd_ee_ff"
        ].config_entry_id
        == config_entry.entry_id
    )
    unsub()
    assert dispatches == [
        {"ip": "5.4.3.2", "mac": "aa:bb:cc:dd:ee:ff", "host_name": "connected"}
    ]


async def test_entity_has_device_info(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test a scanner entity with device info."""

    class DeviceInfoScannerEntity(MockScannerEntity):
        """Test scanner entity with device info."""

        @property
        def device_info(self) -> dr.DeviceInfo:
            """Return device info."""
            return dr.DeviceInfo(
                connections={(dr.CONNECTION_NETWORK_MAC, TEST_MAC_ADDRESS)},
                identifiers={(TEST_DOMAIN, "device1")},
                name="Test Device",
                manufacturer="manufacturer",
                model="model",
            )

    scanner_entity = DeviceInfoScannerEntity(
        ip_address="5.4.3.2",
        mac_address=TEST_MAC_ADDRESS,
    )

    config_entry = await create_mock_platform(hass, config_entry, [scanner_entity])

    assert (
        len(hass.states.async_entity_ids(domain_filter=DOMAIN)) == 1
    )  # should be enabled
    assert len(entity_registry.entities) == 1
    assert (
        entity_registry.entities[f"{DOMAIN}.test_device"].config_entry_id
        == config_entry.entry_id
    )


async def test_tracker_entity_unavailable(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    entity_id: str,
) -> None:
    """Test unavailable tracker entity does not fail on bad latitude/longitude."""

    class _MockTrackerEntity(MockTrackerEntity):
        """Test tracker entity that starts with unavailable state."""

        _attr_available = False

        @property
        def latitude(self) -> float | None:
            """Return latitude value of the device."""
            raise ValueError("Upstream error")

        @property
        def longitude(self) -> float | None:
            """Return longitude value of the device."""
            raise ValueError("Upstream error")

    tracker_entity = _MockTrackerEntity()
    tracker_entity.entity_id = entity_id

    config_entry = await create_mock_platform(hass, config_entry, [tracker_entity])
    assert config_entry.state is ConfigEntryState.LOADED

    await hass.async_block_till_done()

    state = hass.states.get(entity_id)
    assert state
    assert state.state == "unavailable"
    # Capability attributes are reported even when the entity is unavailable.
    assert state.attributes == {ATTR_TRACKING_TYPE: TrackingType.POSITION}
