from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from bleak.backends.device import BLEDevice

from switchbot import SwitchBotAdvertisement, SwitchbotModel
from switchbot.const.light import ColorMode
from switchbot.devices import light_strip
from switchbot.devices.base_light import SwitchbotBaseLight
from switchbot.devices.device import SwitchbotEncryptedDevice, SwitchbotOperationError

from . import (
    FLOOR_LAMP_INFO,
    RGBICWW_FLOOR_LAMP_INFO,
    RGBICWW_STRIP_LIGHT_INFO,
    STRIP_LIGHT_3_INFO,
)
from .test_adv_parser import AdvTestCase, generate_ble_device


@pytest.fixture(
    params=[
        (STRIP_LIGHT_3_INFO, light_strip.SwitchbotStripLight3),
        (FLOOR_LAMP_INFO, light_strip.SwitchbotStripLight3),
        (RGBICWW_STRIP_LIGHT_INFO, light_strip.SwitchbotRgbicLight),
        (RGBICWW_FLOOR_LAMP_INFO, light_strip.SwitchbotRgbicLight),
    ]
)
def device_case(request):
    return request.param


@pytest.fixture
def expected_effects(device_case):
    adv_info, _dev_cls = device_case
    EXPECTED = {
        SwitchbotModel.STRIP_LIGHT_3: ("christmas", "halloween", "sunset"),
        SwitchbotModel.FLOOR_LAMP: ("christmas", "halloween", "sunset"),
        SwitchbotModel.RGBICWW_STRIP_LIGHT: ("romance", "energy", "heartbeat"),
        SwitchbotModel.RGBICWW_FLOOR_LAMP: ("romance", "energy", "heartbeat"),
    }
    return EXPECTED[adv_info.modelName]


def create_device_for_command_testing(
    adv_info: AdvTestCase,
    dev_cls: type[SwitchbotBaseLight],
    init_data: dict | None = None,
):
    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
    device = dev_cls(
        ble_device, "ff", "ffffffffffffffffffffffffffffffff", model=adv_info.modelName
    )
    device.update_from_advertisement(
        make_advertisement_data(ble_device, adv_info, init_data)
    )
    device._send_command = AsyncMock()
    device._check_command_result = MagicMock()
    device.update = AsyncMock()
    return device


def make_advertisement_data(
    ble_device: BLEDevice, adv_info: AdvTestCase, init_data: dict | None = None
):
    """Set advertisement data with defaults."""
    if init_data is None:
        init_data = {}

    return SwitchBotAdvertisement(
        address="aa:bb:cc:dd:ee:ff",
        data={
            "rawAdvData": adv_info.service_data,
            "data": adv_info.data | init_data,
            "isEncrypted": False,
            "model": adv_info.model,
            "modelFriendlyName": adv_info.modelFriendlyName,
            "modelName": adv_info.modelName,
        },
        device=ble_device,
        rssi=-80,
        active=True,
    )


@pytest.mark.asyncio
async def test_default_info(device_case, expected_effects):
    """Test default initialization of the strip light."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)

    assert device.rgb is None

    device._state = {"r": 30, "g": 0, "b": 0, "cw": 3200}

    assert device.is_on() is True
    assert device.on is True
    assert device.color_mode == ColorMode.RGB
    assert device.color_modes == {
        ColorMode.RGB,
        ColorMode.COLOR_TEMP,
    }
    assert device.rgb == (30, 0, 0)
    assert device.color_temp == 3200
    assert device.brightness == adv_info.data["brightness"]
    assert device.min_temp == 2700
    assert device.max_temp == 6500
    # Check that effect list contains expected lowercase effect names
    effect_list = device.get_effect_list
    assert effect_list is not None
    assert all(effect.islower() for effect in effect_list)
    # Verify some known effects are present
    for effect in expected_effects:
        assert effect in effect_list


@pytest.mark.asyncio
@pytest.mark.parametrize(
    ("basic_info", "version_info"), [(True, False), (False, True), (False, False)]
)
async def test_get_basic_info_returns_none(basic_info, version_info, device_case):
    """Test that get_basic_info returns None if no data is available."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)

    device._send_command = AsyncMock(side_effect=[version_info, basic_info])
    device._check_command_result = MagicMock(
        side_effect=[bool(version_info), bool(basic_info)]
    )

    assert await device.get_basic_info() is None


@pytest.mark.asyncio
@pytest.mark.parametrize(
    ("info_data", "result"),
    [
        (
            {
                "basic_info": b"\x01\x00<\xff\x00\xd8\x00\x19d\x00\x03",
                "version_info": b"\x01\x01\n",
            },
            [False, 60, 255, 0, 216, 6500, 3, 1.0],
        ),
        (
            {
                "basic_info": b"\x01\x80NK\xff:\x00\x19d\xff\x02",
                "version_info": b"\x01\x01\n",
            },
            [True, 78, 75, 255, 58, 6500, 2, 1.0],
        ),
        (
            {
                "basic_info": b"\x01\x80$K\xff:\x00\x13\xf9\xff\x06",
                "version_info": b"\x01\x01\n",
            },
            [True, 36, 75, 255, 58, 5113, 6, 1.0],
        ),
    ],
)
async def test_strip_light_get_basic_info(info_data, result, device_case):
    """Test getting basic info from the strip light."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)

    device._send_command = AsyncMock(
        side_effect=[info_data["version_info"], info_data["basic_info"]]
    )
    device._check_command_result = MagicMock(side_effect=[True, True])
    info = await device.get_basic_info()

    assert info["isOn"] is result[0]
    assert info["brightness"] == result[1]
    assert info["r"] == result[2]
    assert info["g"] == result[3]
    assert info["b"] == result[4]
    assert info["cw"] == result[5]
    assert info["color_mode"] == result[6]
    assert info["firmware"] == result[7]


@pytest.mark.asyncio
async def test_set_color_temp(device_case):
    """Test setting color temperature."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)

    await device.set_color_temp(50, 3000)

    device._send_command.assert_called_with(
        device._set_color_temp_command.format("320BB8")
    )


@pytest.mark.asyncio
async def test_turn_on(device_case):
    """Test turning on the strip light."""
    init_data = {"isOn": True}
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls, init_data)

    await device.turn_on()

    device._send_command.assert_called_with(device._turn_on_command)

    assert device.is_on() is True


@pytest.mark.asyncio
async def test_turn_off(device_case):
    """Test turning off the strip light."""
    init_data = {"isOn": False}
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls, init_data)

    await device.turn_off()

    device._send_command.assert_called_with(device._turn_off_command)

    assert device.is_on() is False


@pytest.mark.asyncio
async def test_set_brightness(device_case):
    """Test setting brightness."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)

    await device.set_brightness(75)

    device._send_command.assert_called_with(device._set_brightness_command.format("4B"))


@pytest.mark.asyncio
async def test_set_rgb(device_case):
    """Test setting RGB values."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)

    await device.set_rgb(100, 255, 128, 64)

    device._send_command.assert_called_with(device._set_rgb_command.format("64FF8040"))


@pytest.mark.asyncio
async def test_set_effect_with_invalid_effect(device_case):
    """Test setting an invalid effect."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)

    with pytest.raises(
        SwitchbotOperationError, match="Effect invalid_effect not supported"
    ):
        await device.set_effect("invalid_effect")


@pytest.mark.asyncio
async def test_set_effect_with_valid_effect(device_case):
    """Test setting a valid effect."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)
    device._send_multiple_commands = AsyncMock()

    await device.set_effect("christmas")

    device._send_multiple_commands.assert_called_with(device._effect_dict["christmas"])

    assert device.get_effect() == "christmas"


@pytest.mark.asyncio
async def test_effect_list_contains_lowercase_names(device_case, expected_effects):
    """Test that all effect names in get_effect_list are lowercase."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)

    effect_list = device.get_effect_list

    assert effect_list is not None, "Effect list should not be None"
    # All effect names should be lowercase
    for effect_name in effect_list:
        assert effect_name.islower(), f"Effect name '{effect_name}' is not lowercase"
    # Verify some known effects are present
    for expected_effect in expected_effects:
        assert expected_effect in effect_list, (
            f"Expected effect '{expected_effect}' not found"
        )


@pytest.mark.asyncio
async def test_set_effect_normalizes_case(device_case):
    """Test that set_effect normalizes effect names to lowercase."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)
    device._send_multiple_commands = AsyncMock()

    # Test various case combinations
    test_cases = ["CHRISTMAS", "Christmas", "ChRiStMaS", "christmas"]

    for test_effect in test_cases:
        await device.set_effect(test_effect)
        # Should always work regardless of case
        device._send_multiple_commands.assert_called()
        assert device.get_effect() == test_effect  # Stored as provided


@pytest.mark.asyncio
@patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
async def test_verify_encryption_key(mock_parent_verify, device_case):
    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
    key_id = "ff"
    encryption_key = "ffffffffffffffffffffffffffffffff"

    mock_parent_verify.return_value = True

    adv_info, dev_cls = device_case

    result = await dev_cls.verify_encryption_key(
        device=ble_device,
        key_id=key_id,
        encryption_key=encryption_key,
        model=adv_info.modelName,
    )

    mock_parent_verify.assert_awaited_once_with(
        ble_device,
        key_id,
        encryption_key,
        adv_info.modelName,
    )

    assert result is True


def create_strip_light_device(init_data: dict | None = None):
    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
    return light_strip.SwitchbotLightStrip(ble_device)


@pytest.mark.asyncio
async def test_strip_light_supported_color_modes():
    """Test that the strip light supports the expected color modes."""
    device = create_strip_light_device()

    assert device.color_modes == {
        ColorMode.RGB,
    }


@pytest.mark.asyncio
@pytest.mark.parametrize(
    ("commands", "results", "final_result"),
    [
        (("command1", "command2"), [(b"\x01", False), (None, False)], False),
        (("command1", "command2"), [(None, False), (b"\x01", True)], True),
        (("command1", "command2"), [(b"\x01", True), (b"\x01", False)], True),
    ],
)
async def test_send_multiple_commands(commands, results, final_result, device_case):
    """Test sending multiple commands."""
    adv_info, dev_cls = device_case
    device = create_device_for_command_testing(adv_info, dev_cls)

    device._send_command = AsyncMock(side_effect=[r[0] for r in results])

    device._check_command_result = MagicMock(side_effect=[r[1] for r in results])

    result = await device._send_multiple_commands(list(commands))

    assert result is final_result


@pytest.mark.asyncio
async def test_unimplemented_color_mode():
    class TestDevice(SwitchbotBaseLight):
        pass

    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
    device = TestDevice(ble_device)

    with pytest.raises(NotImplementedError):
        _ = device.color_mode


@pytest.mark.asyncio
async def test_exception_with_wrong_model():
    class TestDevice(SwitchbotBaseLight):
        def __init__(self, device: BLEDevice, model: str = "unknown") -> None:
            super().__init__(device, model=model)

    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
    device = TestDevice(ble_device)

    with pytest.raises(
        SwitchbotOperationError,
        match="Current device aa:bb:cc:dd:ee:ff does not support this functionality",
    ):
        await device.set_rgb(100, 255, 128, 64)
