"""Tests for the standalone DDP LEDClient wrapper and provider."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import numpy as np
import pytest

from ledgrab.core.devices.ddp_led_client import (
    DEFAULT_DDP_PORT,
    DDPLEDClient,
    parse_ddp_url,
)
from ledgrab.core.devices.ddp_provider import DDPDeviceProvider
from ledgrab.core.devices.device_config import DDPConfig
from ledgrab.core.devices.led_client import ProviderDeps

# ============================================================================
# parse_ddp_url
# ============================================================================


@pytest.mark.parametrize(
    "url,expected",
    [
        ("ddp://192.168.1.50", ("192.168.1.50", DEFAULT_DDP_PORT)),
        ("ddp://192.168.1.50:4048", ("192.168.1.50", 4048)),
        ("ddp://192.168.1.50:5000", ("192.168.1.50", 5000)),
        ("192.168.1.50", ("192.168.1.50", DEFAULT_DDP_PORT)),
        ("192.168.1.50:4048", ("192.168.1.50", 4048)),
        ("pixelblaze.local", ("pixelblaze.local", DEFAULT_DDP_PORT)),
        ("ddp://pixelblaze.local:9999", ("pixelblaze.local", 9999)),
    ],
)
def test_parse_ddp_url_accepts_common_forms(url, expected):
    assert parse_ddp_url(url) == expected


@pytest.mark.parametrize("url", ["", "   ", "ddp://", "://192.168.1.1"])
def test_parse_ddp_url_rejects_empty_or_hostless(url):
    with pytest.raises(ValueError):
        parse_ddp_url(url)


# ============================================================================
# DDPLEDClient
# ============================================================================


def _make_connected_client(led_count: int = 3, rgbw: bool = False) -> DDPLEDClient:
    """Build a DDPLEDClient with its underlying DDPClient transport mocked."""
    client = DDPLEDClient("ddp://127.0.0.1", led_count=led_count, rgbw=rgbw)
    # Avoid real UDP socket creation — stub the DDPClient layer.
    inner = MagicMock()
    inner.send_pixels_numpy = MagicMock()
    inner.close = AsyncMock()
    client._ddp = inner
    client._connected = True
    return client


def test_supports_fast_send_is_true():
    client = DDPLEDClient("ddp://127.0.0.1", led_count=1)
    assert client.supports_fast_send is True


def test_send_pixels_fast_pushes_pixels():
    client = _make_connected_client(led_count=3)
    pixels = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]], dtype=np.uint8)

    client.send_pixels_fast(pixels)

    client._ddp.send_pixels_numpy.assert_called_once()
    sent = client._ddp.send_pixels_numpy.call_args[0][0]
    np.testing.assert_array_equal(sent, pixels)


def test_send_pixels_fast_applies_brightness():
    client = _make_connected_client(led_count=1)
    pixels = np.array([[200, 100, 50]], dtype=np.uint8)

    client.send_pixels_fast(pixels, brightness=128)

    sent = client._ddp.send_pixels_numpy.call_args[0][0]
    # 200 * 128 // 255 = 100, 100 * 128 // 255 = 50, 50 * 128 // 255 = 25
    np.testing.assert_array_equal(sent, np.array([[100, 50, 25]], dtype=np.uint8))


def test_send_pixels_fast_brightness_full_is_passthrough():
    client = _make_connected_client(led_count=1)
    pixels = np.array([[200, 100, 50]], dtype=np.uint8)

    client.send_pixels_fast(pixels, brightness=255)

    sent = client._ddp.send_pixels_numpy.call_args[0][0]
    np.testing.assert_array_equal(sent, pixels)


def test_send_pixels_fast_brightness_zero_blanks_all():
    client = _make_connected_client(led_count=2)
    pixels = np.array([[200, 100, 50], [255, 255, 255]], dtype=np.uint8)

    client.send_pixels_fast(pixels, brightness=0)

    sent = client._ddp.send_pixels_numpy.call_args[0][0]
    np.testing.assert_array_equal(sent, np.zeros_like(pixels))


@pytest.mark.asyncio
async def test_async_send_pixels_accepts_list():
    client = _make_connected_client(led_count=2)

    await client.send_pixels([(1, 2, 3), (4, 5, 6)])

    sent = client._ddp.send_pixels_numpy.call_args[0][0]
    np.testing.assert_array_equal(sent, np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint8))


def test_send_pixels_fast_when_not_connected_raises():
    client = DDPLEDClient("ddp://127.0.0.1", led_count=1)
    with pytest.raises(RuntimeError, match="not connected"):
        client.send_pixels_fast([(1, 2, 3)])


def test_send_pixels_fast_accepts_flat_array():
    """A flat 1-D RGB byte array reshapes to (N, 3) before sending."""
    client = _make_connected_client(led_count=2)
    flat = np.array([1, 2, 3, 4, 5, 6], dtype=np.uint8)

    client.send_pixels_fast(flat)

    sent = client._ddp.send_pixels_numpy.call_args[0][0]
    np.testing.assert_array_equal(sent, np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint8))


# ============================================================================
# DDPDeviceProvider
# ============================================================================


def test_provider_device_type():
    provider = DDPDeviceProvider()
    assert provider.device_type == "ddp"


def test_provider_capabilities():
    provider = DDPDeviceProvider()
    caps = provider.capabilities
    assert "manual_led_count" in caps
    assert "health_check" in caps
    # No native power / brightness — DDP has no reply channel.
    assert "power_control" not in caps
    assert "brightness_control" not in caps


@pytest.mark.asyncio
async def test_provider_validate_device_accepts_bare_host():
    provider = DDPDeviceProvider()
    result = await provider.validate_device("192.168.1.50")
    # validate_device returns the empty dict (no LED count to report)
    assert result == {}


@pytest.mark.asyncio
async def test_provider_validate_device_rejects_empty_url():
    provider = DDPDeviceProvider()
    with pytest.raises(ValueError, match="Invalid DDP URL"):
        await provider.validate_device("")


@pytest.mark.asyncio
async def test_provider_discover_returns_empty():
    """DDP has no native discovery — provider must return an empty list, not raise."""
    provider = DDPDeviceProvider()
    assert await provider.discover() == []


def test_provider_create_client_threads_config_fields():
    provider = DDPDeviceProvider()
    config = DDPConfig(
        device_id="device_test",
        device_url="ddp://192.168.1.50:9000",
        led_count=144,
        rgbw=True,
        ddp_port=9000,
        ddp_destination_id=2,
        ddp_color_order=4,  # BGR
    )

    client = provider.create_client(config, deps=ProviderDeps())

    assert isinstance(client, DDPLEDClient)
    assert client.host == "192.168.1.50"
    assert client.port == 9000
    assert client._led_count == 144
    assert client._rgbw is True
    assert client._destination_id == 2
    assert client._color_order == 4


# ============================================================================
# DDPConfig round-trip via Device.to_config()
# ============================================================================


def test_device_to_config_round_trip_ddp():
    """A Device with device_type='ddp' yields a DDPConfig from to_config()."""
    from ledgrab.storage.device_store import Device

    device = Device(
        device_id="device_abc12345",
        name="My Pixelblaze",
        url="ddp://192.168.1.42",
        led_count=300,
        device_type="ddp",
        rgbw=False,
        ddp_port=4048,
        ddp_destination_id=1,
        ddp_color_order=1,
    )

    config = device.to_config()

    assert isinstance(config, DDPConfig)
    assert config.device_type == "ddp"
    assert config.device_url == "ddp://192.168.1.42"
    assert config.led_count == 300
    assert config.ddp_port == 4048
    assert config.ddp_destination_id == 1
    assert config.ddp_color_order == 1


def test_device_to_dict_omits_ddp_defaults():
    """A DDP device with default DDP fields shouldn't pollute the persisted JSON."""
    from ledgrab.storage.device_store import Device

    device = Device(
        device_id="device_abc12345",
        name="Defaults",
        url="ddp://192.168.1.42",
        led_count=10,
        device_type="ddp",
    )

    payload = device.to_dict()

    # Defaults: ddp_port=0, ddp_destination_id=1, ddp_color_order=1 — all omitted.
    assert "ddp_port" not in payload
    assert "ddp_destination_id" not in payload
    assert "ddp_color_order" not in payload


def test_device_to_dict_preserves_non_default_ddp_fields():
    from ledgrab.storage.device_store import Device

    device = Device(
        device_id="device_abc12345",
        name="Custom",
        url="ddp://192.168.1.42",
        led_count=10,
        device_type="ddp",
        ddp_port=4048,
        ddp_destination_id=2,
        ddp_color_order=4,
    )

    payload = device.to_dict()

    assert payload["ddp_port"] == 4048
    assert payload["ddp_destination_id"] == 2
    assert payload["ddp_color_order"] == 4


def test_device_from_dict_round_trip_ddp_fields():
    from ledgrab.storage.device_store import Device

    payload = {
        "id": "device_abc12345",
        "name": "Roundtrip",
        "url": "ddp://192.168.1.42",
        "led_count": 50,
        "device_type": "ddp",
        "ddp_port": 9999,
        "ddp_destination_id": 5,
        "ddp_color_order": 2,
    }

    restored = Device.from_dict(payload)

    assert restored.ddp_port == 9999
    assert restored.ddp_destination_id == 5
    assert restored.ddp_color_order == 2
