"""Tests for the Govee LAN LED client + provider."""

from __future__ import annotations

import json
from unittest.mock import MagicMock

import numpy as np
import pytest

from ledgrab.core.devices.device_config import GoveeConfig
from ledgrab.core.devices.govee_client import (
    GOVEE_CONTROL_PORT,
    GoveeClient,
    _average_color,
    _parse_scan_reply,
    parse_govee_url,
)
from ledgrab.core.devices.govee_provider import GoveeDeviceProvider
from ledgrab.core.devices.led_client import ProviderDeps


# ============================================================================
# URL parsing
# ============================================================================


@pytest.mark.parametrize(
    "url,expected",
    [
        ("govee://192.168.1.50", "192.168.1.50"),
        ("192.168.1.50", "192.168.1.50"),
        ("bulb.local", "bulb.local"),
        ("govee://office-light.lan", "office-light.lan"),
    ],
)
def test_parse_govee_url(url, expected):
    assert parse_govee_url(url) == expected


@pytest.mark.parametrize("url", ["", "   ", "govee://", "://192.168.1.1"])
def test_parse_govee_url_rejects_empty(url):
    with pytest.raises(ValueError):
        parse_govee_url(url)


# ============================================================================
# Scan reply parsing
# ============================================================================


def test_parse_scan_reply_extracts_data_dict():
    raw = json.dumps(
        {
            "msg": {
                "cmd": "scan",
                "data": {
                    "ip": "192.168.1.50",
                    "device": "AA:BB:CC:DD:EE:FF",
                    "sku": "H6076",
                    "wifiVersionSoft": "2.3.4",
                },
            }
        }
    ).encode("utf-8")
    parsed = _parse_scan_reply(raw)
    assert parsed is not None
    assert parsed["ip"] == "192.168.1.50"
    assert parsed["sku"] == "H6076"
    assert parsed["device"] == "AA:BB:CC:DD:EE:FF"


def test_parse_scan_reply_rejects_non_scan_cmd():
    raw = json.dumps({"msg": {"cmd": "devStatus", "data": {"onOff": 1}}}).encode("utf-8")
    assert _parse_scan_reply(raw) is None


def test_parse_scan_reply_rejects_malformed_json():
    assert _parse_scan_reply(b"not json") is None
    assert _parse_scan_reply(b"") is None
    assert _parse_scan_reply(b'{"msg":') is None


def test_parse_scan_reply_rejects_non_dict_payload():
    raw = json.dumps(["not", "a", "dict"]).encode("utf-8")
    assert _parse_scan_reply(raw) is None


# ============================================================================
# Helpers
# ============================================================================


def test_average_color_numpy():
    pixels = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]], dtype=np.uint8)
    assert _average_color(pixels) == (40, 50, 60)


def test_average_color_list_and_empty():
    assert _average_color([(10, 0, 0), (20, 0, 0), (30, 0, 0)]) == (20, 0, 0)
    assert _average_color([]) == (0, 0, 0)


# ============================================================================
# Payload builders
# ============================================================================


def test_build_color_payload_sets_colorTemInKelvin_to_zero():
    """colorTemInKelvin=0 selects RGB mode; otherwise the bulb uses CCT."""
    payload = GoveeClient._build_color_payload(255, 128, 0)
    assert payload == {
        "msg": {
            "cmd": "colorwc",
            "data": {
                "color": {"r": 255, "g": 128, "b": 0},
                "colorTemInKelvin": 0,
            },
        }
    }


def test_build_color_payload_clamps_channel_overflow():
    payload = GoveeClient._build_color_payload(300, -5, 256)
    data = payload["msg"]["data"]["color"]
    assert data == {"r": 300 & 0xFF, "g": -5 & 0xFF, "b": 256 & 0xFF}


def test_build_brightness_payload_clamps_to_1_100():
    assert GoveeClient._build_brightness_payload(0)["msg"]["data"]["value"] == 1
    assert GoveeClient._build_brightness_payload(50)["msg"]["data"]["value"] == 50
    assert GoveeClient._build_brightness_payload(200)["msg"]["data"]["value"] == 100


def test_build_power_payload_uses_integer_1_or_0():
    """Govee expects value 1/0, NOT JSON true/false."""
    assert GoveeClient._build_power_payload(True)["msg"]["data"]["value"] == 1
    assert GoveeClient._build_power_payload(False)["msg"]["data"]["value"] == 0


# ============================================================================
# GoveeClient (mocked transport)
# ============================================================================


def _make_connected_client(min_interval_s: float = 0.0) -> GoveeClient:
    client = GoveeClient("govee://127.0.0.1", led_count=10, min_interval_s=min_interval_s)
    transport = MagicMock()
    transport.sendto = MagicMock()
    transport.close = MagicMock()
    client._transport = transport
    client._protocol = MagicMock()
    client._connected = True
    return client


def _sent_payloads(client: GoveeClient) -> list[dict]:
    return [
        json.loads(call.args[0].decode("utf-8")) for call in client._transport.sendto.call_args_list
    ]


def test_client_targets_control_port_4003():
    client = GoveeClient("govee://192.168.1.50", led_count=1)
    assert client.port == GOVEE_CONTROL_PORT


@pytest.mark.asyncio
async def test_send_pixels_averages_to_colorwc():
    client = _make_connected_client()
    pixels = np.array(
        [[255, 0, 0], [0, 255, 0], [0, 0, 255]],
        dtype=np.uint8,
    )

    await client.send_pixels(pixels)

    payloads = _sent_payloads(client)
    assert len(payloads) == 1
    assert payloads[0]["msg"]["cmd"] == "colorwc"
    # Average of (255,0,0), (0,255,0), (0,0,255) is (85, 85, 85)
    assert payloads[0]["msg"]["data"]["color"] == {"r": 85, "g": 85, "b": 85}
    assert payloads[0]["msg"]["data"]["colorTemInKelvin"] == 0


@pytest.mark.asyncio
async def test_send_pixels_scales_for_brightness():
    client = _make_connected_client()
    pixels = np.array([[200, 100, 50]], dtype=np.uint8)

    await client.send_pixels(pixels, brightness=128)

    payloads = _sent_payloads(client)
    color = payloads[0]["msg"]["data"]["color"]
    assert color["r"] == int(200 * 128 / 255)
    assert color["g"] == int(100 * 128 / 255)
    assert color["b"] == int(50 * 128 / 255)


@pytest.mark.asyncio
async def test_rate_limit_drops_subsequent_frames():
    client = _make_connected_client(min_interval_s=10.0)
    pixels = np.array([[10, 20, 30]], dtype=np.uint8)

    await client.send_pixels(pixels)
    await client.send_pixels(pixels)

    assert len(_sent_payloads(client)) == 1


@pytest.mark.asyncio
async def test_zero_interval_sends_every_frame():
    client = _make_connected_client(min_interval_s=0.0)
    pixels = np.array([[10, 20, 30]], dtype=np.uint8)

    await client.send_pixels(pixels)
    await client.send_pixels(pixels)
    await client.send_pixels(pixels)

    assert len(_sent_payloads(client)) == 3


@pytest.mark.asyncio
async def test_send_pixels_when_not_connected_raises():
    client = GoveeClient("govee://127.0.0.1", led_count=1)
    with pytest.raises(RuntimeError, match="not connected"):
        await client.send_pixels(np.array([[1, 2, 3]], dtype=np.uint8))


def test_send_pixels_fast_runs_synchronously():
    client = _make_connected_client(min_interval_s=0.0)
    pixels = np.array([[10, 20, 30]], dtype=np.uint8)

    client.send_pixels_fast(pixels)

    payloads = _sent_payloads(client)
    assert len(payloads) == 1
    assert payloads[0]["msg"]["cmd"] == "colorwc"


def test_supports_fast_send_is_true():
    assert GoveeClient("govee://127.0.0.1", led_count=1).supports_fast_send is True


@pytest.mark.asyncio
async def test_set_power_sends_turn_cmd():
    client = _make_connected_client()
    await client.set_power(True)
    await client.set_power(False)
    payloads = _sent_payloads(client)
    assert payloads[0] == {"msg": {"cmd": "turn", "data": {"value": 1}}}
    assert payloads[1] == {"msg": {"cmd": "turn", "data": {"value": 0}}}


@pytest.mark.asyncio
async def test_set_brightness_sends_brightness_cmd_clamped():
    client = _make_connected_client()
    await client.set_brightness(5)
    await client.set_brightness(50)
    await client.set_brightness(150)
    payloads = _sent_payloads(client)
    values = [p["msg"]["data"]["value"] for p in payloads]
    assert values == [5, 50, 100]


@pytest.mark.asyncio
async def test_set_color_sends_colorwc():
    client = _make_connected_client()
    await client.set_color(12, 34, 56)
    payloads = _sent_payloads(client)
    assert payloads[0]["msg"]["cmd"] == "colorwc"
    assert payloads[0]["msg"]["data"]["color"] == {"r": 12, "g": 34, "b": 56}


@pytest.mark.asyncio
async def test_close_releases_transport():
    client = _make_connected_client()
    transport = client._transport
    await client.close()
    transport.close.assert_called_once()
    assert client._transport is None
    assert client.is_connected is False


# ============================================================================
# Provider
# ============================================================================


def test_provider_device_type_and_capabilities():
    provider = GoveeDeviceProvider()
    assert provider.device_type == "govee"
    caps = provider.capabilities
    assert "manual_led_count" in caps
    assert "power_control" in caps
    assert "brightness_control" in caps
    assert "single_pixel" in caps


@pytest.mark.asyncio
async def test_provider_validate_accepts_bare_host():
    provider = GoveeDeviceProvider()
    assert await provider.validate_device("192.168.1.50") == {}


@pytest.mark.asyncio
async def test_provider_validate_rejects_empty():
    provider = GoveeDeviceProvider()
    with pytest.raises(ValueError, match="Invalid Govee URL"):
        await provider.validate_device("")


def test_provider_create_client_threads_config():
    provider = GoveeDeviceProvider()
    config = GoveeConfig(
        device_id="device_test",
        device_url="govee://192.168.1.50",
        led_count=30,
        govee_min_interval_ms=100,
    )

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

    assert isinstance(client, GoveeClient)
    assert client.host == "192.168.1.50"
    assert client.port == GOVEE_CONTROL_PORT
    assert client._led_count == 30
    assert client._min_interval_s == pytest.approx(0.1)


@pytest.mark.asyncio
async def test_provider_discover_returns_empty_on_failure(monkeypatch):
    async def _explode(timeout):
        raise OSError("multicast unreachable")

    monkeypatch.setattr("ledgrab.core.devices.govee_provider.discover_govee_devices", _explode)
    provider = GoveeDeviceProvider()
    assert await provider.discover() == []


@pytest.mark.asyncio
async def test_provider_discover_maps_replies_to_discovered_devices(monkeypatch):
    async def _fake(timeout):
        return [
            {
                "ip": "192.168.1.50",
                "device": "AA:BB:CC:DD:EE:FF",
                "sku": "H6076",
                "version": "2.3.4",
            },
            # Missing IP — should be skipped.
            {"ip": "", "device": "00:00", "sku": "H1234"},
        ]

    monkeypatch.setattr("ledgrab.core.devices.govee_provider.discover_govee_devices", _fake)
    provider = GoveeDeviceProvider()
    results = await provider.discover()

    assert len(results) == 1
    [bulb] = results
    assert bulb.device_type == "govee"
    assert bulb.url == "govee://192.168.1.50"
    assert bulb.ip == "192.168.1.50"
    assert bulb.mac == "AA:BB:CC:DD:EE:FF"
    assert bulb.version == "2.3.4"
    assert "h6076" in bulb.name.lower()


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


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

    device = Device(
        device_id="device_abc12345",
        name="Govee Immersion",
        url="govee://192.168.1.42",
        led_count=30,
        device_type="govee",
        govee_min_interval_ms=100,
    )

    config = device.to_config()

    assert isinstance(config, GoveeConfig)
    assert config.device_url == "govee://192.168.1.42"
    assert config.led_count == 30
    assert config.govee_min_interval_ms == 100


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

    device = Device(
        device_id="device_abc12345",
        name="Default",
        url="govee://192.168.1.42",
        led_count=1,
        device_type="govee",
    )
    assert "govee_min_interval_ms" not in device.to_dict()


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

    device = Device(
        device_id="device_abc12345",
        name="Custom",
        url="govee://192.168.1.42",
        led_count=1,
        device_type="govee",
        govee_min_interval_ms=200,
    )
    assert device.to_dict()["govee_min_interval_ms"] == 200


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

    restored = Device.from_dict(
        {
            "id": "device_abc12345",
            "name": "Roundtrip",
            "url": "govee://10.0.0.1",
            "led_count": 1,
            "device_type": "govee",
            "govee_min_interval_ms": 150,
        }
    )
    assert restored.govee_min_interval_ms == 150
