"""Shared fixtures for all tests."""

from __future__ import annotations

import os
import shutil
import socket
from collections.abc import Generator
from pathlib import Path
from typing import ClassVar

import pytest
from xprocess import ProcessStarter

from lifx.api import DeviceGroup
from lifx.network.connection import DeviceConnection


def get_free_port() -> int:
    """Get a free UDP port."""
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
        s.bind(("127.0.0.1", 0))
        return s.getsockname()[1]


def find_lifx_emulator() -> Path | None:
    """Find the lifx-emulator executable.

    Returns:
        Path to lifx-emulator executable, or None if not found
    """
    # Check system PATH
    system_path = shutil.which("lifx-emulator")
    if system_path:
        return Path(system_path)

    return None


@pytest.fixture(scope="session")
def emulator_available() -> bool:
    """Check if lifx-emulator is available."""
    return find_lifx_emulator() is not None


@pytest.fixture(scope="session")
def emulator_server(emulator_available: bool, xprocess) -> Generator[int]:
    """Start lifx-emulator as a subprocess for the entire test session.

    The emulator starts once at the beginning and stops when all tests complete.
    This significantly reduces test overhead compared to per-test startup.

    Only starts if lifx-emulator is available. Tests that require the emulator
    should check emulator_available or will be skipped automatically.

    Uses pytest-xprocess for robust process management and cleanup.

    External emulator mode:
        Set LIFX_EMULATOR_EXTERNAL=1 to skip starting the emulator subprocess.
        Use LIFX_EMULATOR_PORT to specify the port (default: 56700).
        This is useful for testing against actual hardware or a manually managed
        emulator instance with custom configuration.

    Yields:
        UDP port number where the emulator is listening
    """
    # Check if using external emulator
    use_external = os.environ.get("LIFX_EMULATOR_EXTERNAL", "").lower() in (
        "1",
        "true",
        "yes",
    )

    if use_external:
        # Use external emulator - don't start subprocess
        port = int(os.environ.get("LIFX_EMULATOR_PORT", "56700"))
        yield port
        # No cleanup needed for external emulator
        return

    # Standard mode: use xprocess to start emulator subprocess
    if not emulator_available:
        pytest.skip("lifx-emulator not available")

    emulator_path = find_lifx_emulator()
    if not emulator_path:
        pytest.skip("Failed to find lifx-emulator")

    port = get_free_port()

    class EmulatorServer(ProcessStarter):
        """Process starter for use with pytest-xprocess."""

        pattern = "LIFX emulated server listening"
        args: ClassVar[list[str]] = [
            str(emulator_path),
            "--bind",
            "127.0.0.1",  # bind emulator to localhost
            "--port",
            str(port),
            "--color",
            "1",  # 1 color light
            "--multizone",
            "2",  # 2 multizone devices
            "--tile",
            "1",  # 1 tile device
            "--hev",
            "1",  # 1 HEV light
            "--infrared",
            "1",  # 1 infrared light
            "--color-temperature",
            "1",  # 1 color temperature light
        ]

        # Terminate the process on interrupt
        terminate_on_interrupt = True

    # Use xprocess to manage the emulator subprocess
    _ = xprocess.ensure("lifx_emulator", EmulatorServer)

    yield port

    xprocess.getinfo("lifx_emulator").terminate()


@pytest.fixture(scope="session")
def emulator_devices(emulator_server: int) -> DeviceGroup:
    """Return a DeviceGroup with the 7 hardcoded emulated devices.

    This fixture hard-codes the seven devices created by the emulator to avoid
    the overhead of running discovery for every test. All devices connect to
    127.0.0.1 on the emulator's port.

    Returns:
        DeviceGroup containing the 7 emulated devices:
        - 2 regular Light devices (d073d5000001, d073d5000002)
        - 1 InfraredLight (d073d5000003)
        - 1 HevLight (d073d5000004)
        - 2 MultiZoneLight devices (d073d5000005, d073d5000006)
        - 1 TileDevice (d073d5000007)
    """
    import ipaddress

    from lifx.devices import HevLight, InfraredLight, Light, MultiZoneLight, TileDevice

    # Temporarily allow localhost for device creation
    original_is_loopback = ipaddress.IPv4Address.is_loopback.fget

    def fake_is_loopback(_addr_self):
        return False

    # Patch the property
    ipaddress.IPv4Address.is_loopback = property(fake_is_loopback)

    try:
        devices = [
            Light(serial="d073d5000001", ip="127.0.0.1", port=emulator_server),
            Light(serial="d073d5000002", ip="127.0.0.1", port=emulator_server),
            InfraredLight(serial="d073d5000003", ip="127.0.0.1", port=emulator_server),
            HevLight(serial="d073d5000004", ip="127.0.0.1", port=emulator_server),
            MultiZoneLight(serial="d073d5000005", ip="127.0.0.1", port=emulator_server),
            MultiZoneLight(serial="d073d5000006", ip="127.0.0.1", port=emulator_server),
            TileDevice(serial="d073d5000007", ip="127.0.0.1", port=emulator_server),
        ]
        return DeviceGroup(devices)
    finally:
        # Restore original
        ipaddress.IPv4Address.is_loopback = property(original_is_loopback)


@pytest.fixture(autouse=True)
def allow_localhost_for_tests(monkeypatch):
    """Allow localhost IPs for testing with emulator.

    The Device class normally rejects localhost IPs, but for testing with
    the emulator running on 127.0.0.1, we need to bypass this validation.
    """
    import ipaddress

    from lifx.devices.base import Device

    original_init = Device.__init__

    def patched_init(self, *args, **kwargs):
        # Temporarily replace is_loopback check
        original_is_loopback = ipaddress.IPv4Address.is_loopback.fget

        def fake_is_loopback(_addr_self):
            # Allow loopback addresses in tests
            return False

        # Monkeypatch the property
        monkeypatch.setattr(
            ipaddress.IPv4Address, "is_loopback", property(fake_is_loopback)
        )

        try:
            original_init(self, *args, **kwargs)
        finally:
            # Restore original
            monkeypatch.setattr(
                ipaddress.IPv4Address, "is_loopback", property(original_is_loopback)
            )

    monkeypatch.setattr(Device, "__init__", patched_init)


@pytest.fixture(autouse=True)
async def cleanup_connection_pool():
    """Clean up the connection pool after each test.

    This ensures test isolation by closing all pooled connections
    after each test completes. Without this, stale connections to
    old mock server ports persist across tests.
    """
    yield
    # Close all connections in the pool after test completes
    await DeviceConnection.close_all_connections()
