import logging
from unittest.mock import AsyncMock, Mock, patch

import pytest

from switchbot import SwitchbotModel
from switchbot.const.lock import LockStatus
from switchbot.devices import lock

from .test_adv_parser import generate_ble_device


def create_device_for_command_testing(model: str):
    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
    return lock.SwitchbotLock(
        ble_device, "ff", "ffffffffffffffffffffffffffffffff", model=model
    )


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_lock_init(model: str):
    """Test the initialization of the lock device."""
    device = create_device_for_command_testing(model)
    assert device._model == model


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.AIR_PURIFIER,
    ],
)
def test_lock_init_with_invalid_model(model: str):
    """Test that initializing with an invalid model raises ValueError."""
    with pytest.raises(
        ValueError, match="initializing SwitchbotLock with a non-lock model"
    ):
        create_device_for_command_testing(model)


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_verify_encryption_key(model: str):
    """Test verify_encryption_key method."""
    ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
    with patch("switchbot.devices.lock.super") as mock_super:
        mock_super().verify_encryption_key = AsyncMock(return_value=True)
        result = await lock.SwitchbotLock.verify_encryption_key(
            ble_device, "key_id", "encryption_key", model
        )
        assert result is True


@pytest.mark.asyncio
@pytest.mark.parametrize(
    ("model", "command"),
    [
        (SwitchbotModel.LOCK, b"W\x0fN\x01\x01\x10\x80"),
        (SwitchbotModel.LOCK_LITE, b"W\x0fN\x01\x01\x10\x81"),
        (SwitchbotModel.LOCK_PRO, b"W\x0fN\x01\x01\x10\x85"),
        (SwitchbotModel.LOCK_ULTRA, b"W\x0fN\x01\x01\x10\x86"),
    ],
)
async def test_lock(model: str, command: bytes):
    """Test lock method."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=LockStatus.UNLOCKED)
    with (
        patch.object(device, "_send_command", return_value=b"\x01\x00"),
        patch.object(device, "_enable_notifications", return_value=True),
        patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
    ):
        result = await device.lock()
        assert result is True


@pytest.mark.asyncio
@pytest.mark.parametrize(
    ("model", "command"),
    [
        (SwitchbotModel.LOCK, b"W\x0fN\x01\x01\x10\x80"),
        (SwitchbotModel.LOCK_LITE, b"W\x0fN\x01\x01\x10\x81"),
        (SwitchbotModel.LOCK_PRO, b"W\x0fN\x01\x01\x10\x84"),
        (SwitchbotModel.LOCK_ULTRA, b"W\x0fN\x01\x01\x10\x83"),
    ],
)
async def test_unlock(model: str, command: bytes):
    """Test unlock method."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=LockStatus.LOCKED)
    with (
        patch.object(device, "_send_command", return_value=b"\x01\x00"),
        patch.object(device, "_enable_notifications", return_value=True),
        patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
    ):
        result = await device.unlock()
        assert result is True


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_unlock_without_unlatch(model: str):
    """Test unlock_without_unlatch method."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=LockStatus.LOCKED)
    with (
        patch.object(device, "_send_command", return_value=b"\x01\x00"),
        patch.object(device, "_enable_notifications", return_value=True),
        patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
    ):
        result = await device.unlock_without_unlatch()
        assert result is True


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_get_basic_info(model: str):
    """Test get_basic_info method."""
    device = create_device_for_command_testing(model)
    lock_data = b"\x00\x80\x00\x00\x00\x00\x00\x00"
    basic_data = b"\x00\x64\x01"
    with (
        patch.object(device, "_get_lock_info", return_value=lock_data),
        patch.object(device, "_get_basic_info", return_value=basic_data),
    ):
        result = await device.get_basic_info()
        assert result is not None
        assert "battery" in result
        assert "firmware" in result
        assert "calibration" in result
        assert "status" in result


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_get_basic_info_no_lock_data(model: str):
    """Test get_basic_info when no lock data is returned."""
    device = create_device_for_command_testing(model)
    with patch.object(device, "_get_lock_info", return_value=None):
        result = await device.get_basic_info()
        assert result is None


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_get_basic_info_no_basic_data(model: str):
    """Test get_basic_info when no basic data is returned."""
    device = create_device_for_command_testing(model)
    lock_data = b"\x00\x80\x00\x00\x00\x00\x00\x00"
    with (
        patch.object(device, "_get_lock_info", return_value=lock_data),
        patch.object(device, "_get_basic_info", return_value=None),
    ):
        result = await device.get_basic_info()
        assert result is None


def test_parse_basic_data():
    """Test _parse_basic_data method."""
    device = create_device_for_command_testing(SwitchbotModel.LOCK)
    basic_data = b"\x00\x64\x01"
    result = device._parse_basic_data(basic_data)
    assert result["battery"] == 100
    assert result["firmware"] == 0.1


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_is_calibrated(model: str):
    """Test is_calibrated method."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=True)
    assert device.is_calibrated() is True


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_get_lock_status(model: str):
    """Test get_lock_status method."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=LockStatus.LOCKED)
    assert device.get_lock_status() == LockStatus.LOCKED


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_is_door_open(model: str):
    """Test is_door_open method."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=True)
    assert device.is_door_open() is True


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_is_unclosed_alarm_on(model: str):
    """Test is_unclosed_alarm_on method."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=True)
    assert device.is_unclosed_alarm_on() is True


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_is_unlocked_alarm_on(model: str):
    """Test is_unlocked_alarm_on method."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=True)
    assert device.is_unlocked_alarm_on() is True


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
    ],
)
def test_is_auto_lock_paused(model: str):
    """Test is_auto_lock_paused method."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=True)
    assert device.is_auto_lock_paused() is True


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_is_night_latch_enabled(model: str):
    """Test is_night_latch_enabled method."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=True)
    assert device.is_night_latch_enabled() is True


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_get_lock_info(model: str):
    """Test _get_lock_info method."""
    device = create_device_for_command_testing(model)
    expected_data = b"\x01\x00\x80\x00\x00\x00\x00\x00"
    with patch.object(device, "_send_command", return_value=expected_data):
        result = await device._get_lock_info()
        assert result == expected_data


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_get_lock_info_failure(model: str):
    """Test _get_lock_info method when command fails."""
    device = create_device_for_command_testing(model)
    with patch.object(device, "_send_command", return_value=b"\x00\x00"):
        result = await device._get_lock_info()
        assert result is None


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_enable_notifications(model: str):
    """Test _enable_notifications method."""
    device = create_device_for_command_testing(model)
    with patch.object(device, "_send_command", return_value=b"\x01\x00"):
        result = await device._enable_notifications()
        assert result is True


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_disable_notifications(model: str):
    """Test _disable_notifications method."""
    device = create_device_for_command_testing(model)
    device._notifications_enabled = True
    with patch.object(device, "_send_command", return_value=b"\x01\x00"):
        result = await device._disable_notifications()
        assert result is True
        assert device._notifications_enabled is False


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_disable_notifications_already_disabled(model: str):
    """Test _disable_notifications when already disabled."""
    device = create_device_for_command_testing(model)
    device._notifications_enabled = False
    with patch.object(device, "_send_command") as mock_send:
        result = await device._disable_notifications()
        assert result is True
        mock_send.assert_not_called()


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_notification_handler(model: str):
    """Test _notification_handler method."""
    device = create_device_for_command_testing(model)
    device._notifications_enabled = True
    data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
    with patch.object(device, "_update_lock_status") as mock_update:
        device._notification_handler(0, data)
        mock_update.assert_called_once_with(data)


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_notification_handler_not_enabled(model: str):
    """Test _notification_handler when notifications not enabled."""
    device = create_device_for_command_testing(model)
    device._notifications_enabled = False
    data = bytearray(b"\x01\x00\x00\x00\x80\x00\x00\x00\x00\x00")
    with (
        patch.object(device, "_update_lock_status") as mock_update,
        patch.object(
            device.__class__.__bases__[0], "_notification_handler"
        ) as mock_super,
    ):
        device._notification_handler(0, data)
        mock_update.assert_not_called()
        mock_super.assert_called_once()


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_notification_handler_during_disconnect(
    model: str, caplog: pytest.LogCaptureFixture
) -> None:
    """Test _notification_handler during expected disconnect."""
    device = create_device_for_command_testing(model)
    device._notifications_enabled = True
    device._expected_disconnect = True
    data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
    with (
        patch.object(device, "_update_lock_status") as mock_update,
        caplog.at_level(logging.DEBUG),
    ):
        device._notification_handler(0, data)
        # Should not update lock status during disconnect
        mock_update.assert_not_called()
        # Should log debug message
        assert "Ignoring lock notification during expected disconnect" in caplog.text


@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
def test_update_lock_status(model: str):
    """Test _update_lock_status method."""
    device = create_device_for_command_testing(model)
    data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
    with (
        patch.object(device, "_decrypt", return_value=b"\x80\x00\x00\x00\x00\x00"),
        patch.object(device, "_update_parsed_data", return_value=True),
        patch.object(device, "_reset_disconnect_timer"),
        patch.object(device, "_fire_callbacks"),
    ):
        device._update_lock_status(data)


@pytest.mark.parametrize(
    ("model", "data", "expected"),
    [
        (
            SwitchbotModel.LOCK,
            b"\x80\x00\x00\x00\x00\x00",
            {
                "calibration": True,
                "status": LockStatus.LOCKED,  # (0x80 & 0b01110000) >> 4 = 0 = LOCKED
                "door_open": False,
                "unclosed_alarm": False,
                "unlocked_alarm": False,
            },
        ),
        (
            SwitchbotModel.LOCK_LITE,
            b"\x80\x00\x00\x00\x00\x00",
            {
                "calibration": True,
                "status": LockStatus.LOCKED,  # (0x80 & 0b01110000) >> 4 = 0 = LOCKED
                "unlocked_alarm": False,
            },
        ),
        (
            SwitchbotModel.LOCK_PRO,
            b"\x80\x00\x00\x00\x00\x00",
            {
                "calibration": True,
                "status": LockStatus.LOCKED,  # (0x80 & 0b01111000) >> 3 = 0 = LOCKED
                "door_open": False,
                "unclosed_alarm": False,
                "unlocked_alarm": False,
            },
        ),
        (
            SwitchbotModel.LOCK_ULTRA,
            b"\x88\x10\x00\x00\x00\xc0",
            {
                "calibration": True,
                "status": LockStatus.UNLOCKED,  # (0x88 & 0b01111000) >> 3 = 0x08 >> 3 = 1 = UNLOCKED
                "door_open": True,
                "unclosed_alarm": True,
                "unlocked_alarm": True,
            },
        ),
    ],
)
def test_parse_lock_data(model: str, data: bytes, expected: dict):
    """Test _parse_lock_data static method."""
    result = lock.SwitchbotLock._parse_lock_data(data, model)
    assert result == expected


@pytest.mark.parametrize(
    ("model", "data", "expected"),
    [
        # Test LOCK with different status bits and flags
        (
            SwitchbotModel.LOCK,
            b"\x94\x00\x00\x00\x00\x00",  # Unlocked status (0x10 >> 4 = 1) with door open
            {
                "calibration": True,
                "status": LockStatus.UNLOCKED,
                "door_open": True,
                "unclosed_alarm": False,
                "unlocked_alarm": False,
            },
        ),
        # Test LOCK_LITE without door_open field
        (
            SwitchbotModel.LOCK_LITE,
            b"\x90\x10\x00\x00\x00\x00",  # Unlocked with unlocked alarm
            {
                "calibration": True,
                "status": LockStatus.UNLOCKED,
                "unlocked_alarm": True,
            },
        ),
        # Test LOCK_PRO with new bit positions
        (
            SwitchbotModel.LOCK_PRO,
            b"\x90\x10\x00\x00\x00\xc0",  # New format: status bits 3-6, door open bit 4 of byte 1
            {
                "calibration": True,
                "status": LockStatus.LOCKING,  # (0x90 & 0b01111000) >> 3 = 0x10 >> 3 = 2 (LOCKING)
                "door_open": True,  # bit 4 of byte 1 (0x10)
                "unclosed_alarm": True,  # bit 7 of byte 5 (0xc0)
                "unlocked_alarm": True,  # bit 6 of byte 5 (0xc0)
            },
        ),
        # Test LOCK_ULTRA with same format as PRO
        (
            SwitchbotModel.LOCK_ULTRA,
            b"\x88\x00\x00\x00\x00\x40",  # Unlocked with unlocked alarm only
            {
                "calibration": True,
                "status": LockStatus.UNLOCKED,  # (0x88 & 0b01111000) >> 3 = 0x08 >> 3 = 1
                "door_open": False,
                "unclosed_alarm": False,
                "unlocked_alarm": True,  # bit 6 of byte 5
            },
        ),
    ],
)
def test_parse_lock_data_new_formats(model: str, data: bytes, expected: dict):
    """Test _parse_lock_data with new format changes."""
    result = lock.SwitchbotLock._parse_lock_data(data, model)
    assert result == expected


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_lock_with_update(model: str):
    """Test lock method with status update."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(side_effect=[None, LockStatus.UNLOCKED])
    with (
        patch.object(device, "update", new_callable=AsyncMock),
        patch.object(device, "_send_command", return_value=b"\x01\x00"),
        patch.object(device, "_enable_notifications", return_value=True),
        patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
    ):
        result = await device.lock()
        assert result is True


@pytest.mark.asyncio
@pytest.mark.parametrize(
    ("model", "status"),
    [
        (SwitchbotModel.LOCK, LockStatus.LOCKED),
        (SwitchbotModel.LOCK_LITE, LockStatus.LOCKING),
        (SwitchbotModel.LOCK_PRO, LockStatus.LOCKED),
        (SwitchbotModel.LOCK_ULTRA, LockStatus.LOCKING),
    ],
)
async def test_lock_already_locked(model: str, status: LockStatus):
    """Test lock method when already locked."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=status)
    with patch.object(device, "_send_command") as mock_send:
        result = await device.lock()
        assert result is True
        mock_send.assert_not_called()


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model",
    [
        SwitchbotModel.LOCK,
        SwitchbotModel.LOCK_LITE,
        SwitchbotModel.LOCK_PRO,
        SwitchbotModel.LOCK_ULTRA,
    ],
)
async def test_lock_with_invalid_basic_data(model: str):
    """Test lock method with invalid basic data."""
    device = create_device_for_command_testing(model)
    device._get_adv_value = Mock(return_value=LockStatus.UNLOCKED)
    with (
        patch.object(device, "_send_command", return_value=b"\x01\x00"),
        patch.object(device, "_enable_notifications", return_value=True),
        patch.object(device, "_get_basic_info", return_value=b"\x00"),
    ):
        result = await device.lock()
        assert result is True
