"""Tests for the Nanoleaf OpenAPI LED client + provider."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import httpx
import numpy as np
import pytest

from ledgrab.core.devices.device_config import NanoleafConfig
from ledgrab.core.devices.led_client import PairingNotReady, ProviderDeps
from ledgrab.core.devices.nanoleaf_client import (
    NANOLEAF_PORT,
    NanoleafClient,
    _average_color,
    pair_nanoleaf,
    parse_nanoleaf_url,
    rgb_to_hsb,
)
from ledgrab.core.devices.nanoleaf_provider import NanoleafDeviceProvider

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


@pytest.mark.parametrize(
    "url,expected",
    [
        ("nanoleaf://192.168.1.50", "192.168.1.50"),
        ("nanoleaf://192.168.1.50:16021", "192.168.1.50"),
        ("192.168.1.50", "192.168.1.50"),
        ("nanoleaf://controller.local", "controller.local"),
    ],
)
def test_parse_nanoleaf_url(url, expected):
    assert parse_nanoleaf_url(url) == expected


@pytest.mark.parametrize("url", ["", "   ", "nanoleaf://", "://1.2.3.4"])
def test_parse_nanoleaf_url_rejects_empty(url):
    with pytest.raises(ValueError):
        parse_nanoleaf_url(url)


# ============================================================================
# RGB → HSB conversion (Nanoleaf scale: H 0-360, S 0-100, B 0-100)
# ============================================================================


def test_rgb_to_hsb_pure_red():
    h, s, b = rgb_to_hsb(255, 0, 0)
    assert h == 0
    assert s == 100
    assert b == 100


def test_rgb_to_hsb_pure_green():
    h, s, b = rgb_to_hsb(0, 255, 0)
    assert h == 120
    assert s == 100
    assert b == 100


def test_rgb_to_hsb_pure_blue():
    h, s, b = rgb_to_hsb(0, 0, 255)
    assert h == 240
    assert s == 100
    assert b == 100


def test_rgb_to_hsb_white_zero_saturation():
    h, s, b = rgb_to_hsb(255, 255, 255)
    assert s == 0
    assert b == 100
    # Hue is undefined for pure white; we deterministically pick 0
    assert h == 0


def test_rgb_to_hsb_black_zero_brightness():
    h, s, b = rgb_to_hsb(0, 0, 0)
    assert b == 0
    assert s == 0
    assert h == 0


def test_rgb_to_hsb_clamps_out_of_range():
    h, s, b = rgb_to_hsb(-10, 999, 128)
    assert 0 <= h < 360
    assert 0 <= s <= 100
    assert 0 <= b <= 100


# ============================================================================
# _average_color
# ============================================================================


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)


# ============================================================================
# pair_nanoleaf — HTTP handshake
# ============================================================================


@pytest.mark.asyncio
async def test_pair_nanoleaf_returns_token_on_200(respx_mock):
    respx_mock.post(f"http://1.2.3.4:{NANOLEAF_PORT}/api/v1/new").mock(
        return_value=httpx.Response(200, json={"auth_token": "tok-abc"}),
    )
    token = await pair_nanoleaf("1.2.3.4")
    assert token == "tok-abc"


@pytest.mark.asyncio
async def test_pair_nanoleaf_raises_pairing_not_ready_on_403(respx_mock):
    respx_mock.post(f"http://1.2.3.4:{NANOLEAF_PORT}/api/v1/new").mock(
        return_value=httpx.Response(403, text="not pairing"),
    )
    with pytest.raises(PairingNotReady) as exc:
        await pair_nanoleaf("1.2.3.4")
    # The user-facing message must mention the physical action.
    assert "power button" in str(exc.value).lower()


@pytest.mark.asyncio
async def test_pair_nanoleaf_raises_runtime_error_on_500(respx_mock):
    respx_mock.post(f"http://1.2.3.4:{NANOLEAF_PORT}/api/v1/new").mock(
        return_value=httpx.Response(500, text="boom"),
    )
    with pytest.raises(RuntimeError, match="HTTP 500"):
        await pair_nanoleaf("1.2.3.4")


@pytest.mark.asyncio
async def test_pair_nanoleaf_rejects_missing_token(respx_mock):
    respx_mock.post(f"http://1.2.3.4:{NANOLEAF_PORT}/api/v1/new").mock(
        return_value=httpx.Response(200, json={"not_a_token": "x"}),
    )
    with pytest.raises(RuntimeError, match="no auth_token"):
        await pair_nanoleaf("1.2.3.4")


@pytest.mark.asyncio
async def test_pair_nanoleaf_wraps_transport_error(respx_mock):
    respx_mock.post(f"http://1.2.3.4:{NANOLEAF_PORT}/api/v1/new").mock(
        side_effect=httpx.ConnectError("refused", request=None),
    )
    with pytest.raises(RuntimeError, match="transport failure"):
        await pair_nanoleaf("1.2.3.4")


# ============================================================================
# NanoleafClient (mocked HTTP)
# ============================================================================


def _make_connected_client(token: str = "tok-abc") -> NanoleafClient:
    """Build a NanoleafClient with a mocked httpx.AsyncClient."""
    client = NanoleafClient(
        "nanoleaf://1.2.3.4", led_count=10, auth_token=token, min_interval_s=0.0
    )
    http_mock = MagicMock()
    http_mock.put = AsyncMock(return_value=httpx.Response(204))
    http_mock.aclose = AsyncMock()
    client._http = http_mock
    client._connected = True
    return client


def _last_put_body(client: NanoleafClient) -> dict:
    """Pull the JSON body out of the most recent put() call."""
    assert client._http.put.called
    return client._http.put.call_args.kwargs["json"]


def _last_put_url(client: NanoleafClient) -> str:
    return client._http.put.call_args.args[0]


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

    await client.send_pixels(pixels)

    body = _last_put_body(client)
    assert "hue" in body
    assert "sat" in body
    assert "brightness" in body
    assert body["hue"]["value"] == 0
    assert body["sat"]["value"] == 100
    assert body["brightness"]["value"] == 100
    # duration: 0 keeps the transition instant — critical for ambilight
    assert body["brightness"]["duration"] == 0
    assert "/api/v1/abc123/state" in _last_put_url(client)


@pytest.mark.asyncio
async def test_send_pixels_clamps_brightness_to_at_least_1():
    """Nanoleaf rejects brightness=0; clamping to 1 keeps the API happy."""
    client = _make_connected_client()
    pixels = np.array([[0, 0, 0]], dtype=np.uint8)

    await client.send_pixels(pixels)

    body = _last_put_body(client)
    assert body["brightness"]["value"] == 1


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

    await client.send_pixels(pixels, brightness=128)

    body = _last_put_body(client)
    # 255 * 128/255 = 128 → brightness ~128/255 of full = ~50% of HSB scale
    assert 40 <= body["brightness"]["value"] <= 60


@pytest.mark.asyncio
async def test_send_pixels_when_not_connected_raises():
    client = NanoleafClient("nanoleaf://1.2.3.4", led_count=1, auth_token="abc")
    with pytest.raises(RuntimeError, match="not connected"):
        await client.send_pixels([(1, 2, 3)])


@pytest.mark.asyncio
async def test_set_power_emits_on_value():
    client = _make_connected_client()

    await client.set_power(True)
    await client.set_power(False)

    bodies = [call.kwargs["json"] for call in client._http.put.call_args_list]
    assert bodies[0] == {"on": {"value": True}}
    assert bodies[1] == {"on": {"value": False}}


@pytest.mark.asyncio
async def test_set_brightness_clamps_to_0_100():
    client = _make_connected_client()

    await client.set_brightness(-10)
    await client.set_brightness(50)
    await client.set_brightness(200)

    bodies = [call.kwargs["json"] for call in client._http.put.call_args_list]
    assert bodies[0]["brightness"]["value"] == 0
    assert bodies[1]["brightness"]["value"] == 50
    assert bodies[2]["brightness"]["value"] == 100


@pytest.mark.asyncio
async def test_set_color_emits_hsb_put():
    client = _make_connected_client()

    await client.set_color(255, 0, 0)

    body = _last_put_body(client)
    assert body["hue"]["value"] == 0
    assert body["sat"]["value"] == 100
    assert body["brightness"]["value"] == 100


@pytest.mark.asyncio
async def test_send_pixels_raises_on_non_2xx_response():
    client = _make_connected_client()
    client._http.put = AsyncMock(return_value=httpx.Response(401, text="unauthorized"))

    with pytest.raises(RuntimeError, match="rejected state update"):
        await client.send_pixels(np.array([[1, 2, 3]], dtype=np.uint8))


@pytest.mark.asyncio
async def test_connect_requires_token():
    client = NanoleafClient("nanoleaf://1.2.3.4", led_count=1, auth_token="")
    with pytest.raises(RuntimeError, match="requires an auth_token"):
        await client.connect()


@pytest.mark.asyncio
async def test_close_releases_http_client():
    client = _make_connected_client()
    http = client._http

    await client.close()

    http.aclose.assert_awaited()
    assert client._http is None
    assert client.is_connected is False


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


def test_provider_device_type_and_capabilities():
    provider = NanoleafDeviceProvider()
    assert provider.device_type == "nanoleaf"
    caps = provider.capabilities
    assert "manual_led_count" in caps
    assert "requires_pairing" in caps
    assert "power_control" in caps
    assert "brightness_control" in caps
    assert "single_pixel" in caps


@pytest.mark.asyncio
async def test_provider_pair_device_returns_nanoleaf_token(respx_mock):
    # Use a LAN address; validate_lan_host now rejects public IPs at the
    # provider boundary (review HIGH #4).
    respx_mock.post(f"http://192.168.1.50:{NANOLEAF_PORT}/api/v1/new").mock(
        return_value=httpx.Response(200, json={"auth_token": "ttoken"}),
    )
    provider = NanoleafDeviceProvider()

    fields = await provider.pair_device("nanoleaf://192.168.1.50")

    assert fields == {"nanoleaf_token": "ttoken"}


@pytest.mark.asyncio
async def test_provider_pair_device_propagates_pairing_not_ready(respx_mock):
    respx_mock.post(f"http://192.168.1.50:{NANOLEAF_PORT}/api/v1/new").mock(
        return_value=httpx.Response(403),
    )
    provider = NanoleafDeviceProvider()
    with pytest.raises(PairingNotReady):
        await provider.pair_device("nanoleaf://192.168.1.50")


@pytest.mark.asyncio
async def test_provider_pair_device_rejects_public_ip():
    """validate_lan_host fires before pair_nanoleaf even runs."""
    provider = NanoleafDeviceProvider()
    with pytest.raises(ValueError, match="LedGrab is LAN-only"):
        await provider.pair_device("nanoleaf://1.1.1.1")


@pytest.mark.asyncio
async def test_provider_pair_device_rejects_invalid_url():
    provider = NanoleafDeviceProvider()
    with pytest.raises(ValueError, match="Invalid Nanoleaf URL"):
        await provider.pair_device("")


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


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


def test_provider_create_client_threads_config():
    provider = NanoleafDeviceProvider()
    config = NanoleafConfig(
        device_id="device_test",
        device_url="nanoleaf://192.168.1.50",
        led_count=30,
        nanoleaf_token="abc-token",
        nanoleaf_min_interval_ms=200,
    )

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

    assert isinstance(client, NanoleafClient)
    assert client.host == "192.168.1.50"
    assert client._token == "abc-token"
    assert client._min_interval_s == pytest.approx(0.2)


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


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

    device = Device(
        device_id="device_abc12345",
        name="Office Panels",
        url="nanoleaf://192.168.1.42",
        led_count=9,
        device_type="nanoleaf",
        nanoleaf_token="tok-xyz",
        nanoleaf_min_interval_ms=150,
    )

    config = device.to_config()

    assert isinstance(config, NanoleafConfig)
    assert config.device_url == "nanoleaf://192.168.1.42"
    assert config.nanoleaf_token == "tok-xyz"
    assert config.nanoleaf_min_interval_ms == 150


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

    device = Device(
        device_id="device_abc12345",
        name="Default",
        url="nanoleaf://192.168.1.42",
        led_count=1,
        device_type="nanoleaf",
    )
    payload = device.to_dict()
    assert "nanoleaf_token" not in payload
    assert "nanoleaf_min_interval_ms" not in payload


def test_device_to_dict_encrypts_nanoleaf_token():
    """The auth token must not appear in cleartext in storage."""
    from ledgrab.storage.device_store import Device

    device = Device(
        device_id="device_abc12345",
        name="Encrypted",
        url="nanoleaf://192.168.1.42",
        led_count=1,
        device_type="nanoleaf",
        nanoleaf_token="cleartext-secret-token",
    )

    payload = device.to_dict()

    assert "nanoleaf_token" in payload
    assert payload["nanoleaf_token"] != "cleartext-secret-token"


def test_device_from_dict_decrypts_nanoleaf_token():
    """from_dict + to_dict on an encrypted token must round-trip back to cleartext."""
    from ledgrab.storage.device_store import Device

    original = Device(
        device_id="device_abc12345",
        name="Roundtrip",
        url="nanoleaf://10.0.0.1",
        led_count=1,
        device_type="nanoleaf",
        nanoleaf_token="cleartext-secret-token",
        nanoleaf_min_interval_ms=200,
    )

    restored = Device.from_dict(original.to_dict() | {"id": original.id})

    assert restored.nanoleaf_token == "cleartext-secret-token"
    assert restored.nanoleaf_min_interval_ms == 200
