"""Tests for the cover entity."""

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

import pytest
from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature
from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.tuya_local.const import (
    CONF_DEVICE_ID,
    CONF_PROTOCOL_VERSION,
    CONF_TYPE,
    DOMAIN,
)
from custom_components.tuya_local.cover import TuyaLocalCover, async_setup_entry


@pytest.mark.asyncio
async def test_init_entry(hass):
    """Test the initialisation."""
    entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_TYPE: "garage_door_opener",
            CONF_DEVICE_ID: "dummy",
            CONF_PROTOCOL_VERSION: "auto",
        },
    )
    m_add_entities = Mock()
    m_device = AsyncMock()

    hass.data[DOMAIN] = {
        "dummy": {
            "device": m_device,
        },
    }
    await async_setup_entry(hass, entry, m_add_entities)
    assert type(hass.data[DOMAIN]["dummy"]["cover_garage"]) is TuyaLocalCover
    m_add_entities.assert_called_once()


@pytest.mark.asyncio
async def test_init_entry_fails_if_device_has_no_cover(hass):
    """Test initialisation when device has no matching entity"""
    entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_TYPE: "kogan_heater",
            CONF_DEVICE_ID: "dummy",
            CONF_PROTOCOL_VERSION: "auto",
        },
    )
    m_add_entities = Mock()
    m_device = AsyncMock()

    hass.data[DOMAIN] = {
        "dummy": {
            "device": m_device,
        },
    }
    try:
        await async_setup_entry(hass, entry, m_add_entities)
        assert False
    except ValueError:
        pass
    m_add_entities.assert_not_called()


@pytest.mark.asyncio
async def test_init_entry_fails_if_config_is_missing(hass):
    """Test initialisation when device has no matching entity"""
    entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_TYPE: "non_existing",
            CONF_DEVICE_ID: "dummy",
            CONF_PROTOCOL_VERSION: "auto",
        },
    )
    # although async, the async_add_entities function passed to
    # async_setup_entry is called truly asynchronously. If we use
    # AsyncMock, it expects us to await the result.
    m_add_entities = Mock()
    m_device = AsyncMock()

    hass.data[DOMAIN] = {}
    hass.data[DOMAIN]["dummy"] = {}
    hass.data[DOMAIN]["dummy"]["device"] = m_device
    try:
        await async_setup_entry(hass, entry, m_add_entities)
        assert False
    except ValueError:
        pass
    m_add_entities.assert_not_called()


def _make_cover(
    position=True,
    currentpos=False,
    tiltpos=False,
    control=True,
    action=True,
    open_dp=False,
    control_values=None,
):
    """Create a TuyaLocalCover with mocked internals."""
    if control_values is None:
        control_values = ["open", "close", "stop"]

    device = MagicMock()
    config = MagicMock()
    config.device_class = None
    config.name = "Test Cover"
    config.translation_key = None
    config.translation_only_key = None
    config.translation_placeholders = None
    config.entity_category = None

    dps = {}
    if position:
        dp = MagicMock()
        dp.name = "position"
        dps["position"] = dp
    if currentpos:
        dp = MagicMock()
        dp.name = "current_position"
        dps["current_position"] = dp
    if tiltpos:
        dp = MagicMock()
        dp.name = "tilt_position"
        dps["tilt_position"] = dp
    if control:
        dp = MagicMock()
        dp.name = "control"
        dp.values.return_value = control_values
        dps["control"] = dp
    if action:
        dp = MagicMock()
        dp.name = "action"
        dps["action"] = dp
    if open_dp:
        dp = MagicMock()
        dp.name = "open"
        dps["open"] = dp

    config.dps.return_value = list(dps.values())

    # Patch __init__ to avoid MRO issues with HA entity base classes
    cover = object.__new__(TuyaLocalCover)
    cover._device = device
    cover._config = config
    cover._attr_dps = []
    cover._attr_translation_key = None
    cover._attr_translation_placeholders = None

    cover._position_dp = dps.get("position")
    cover._currentpos_dp = dps.get("current_position")
    cover._tiltpos_dp = dps.get("tilt_position")
    cover._control_dp = dps.get("control")
    cover._action_dp = dps.get("action")
    cover._open_dp = dps.get("open")

    # Build support flags
    cover._support_flags = CoverEntityFeature(0)
    if cover._position_dp:
        cover._support_flags |= CoverEntityFeature.SET_POSITION
    if cover._control_dp:
        vals = cover._control_dp.values(device)
        if "stop" in vals:
            cover._support_flags |= CoverEntityFeature.STOP
        if "open" in vals:
            cover._support_flags |= CoverEntityFeature.OPEN
        if "close" in vals:
            cover._support_flags |= CoverEntityFeature.CLOSE
    if cover._tiltpos_dp:
        cover._support_flags |= CoverEntityFeature.SET_TILT_POSITION

    return cover


class TestSupportedFeatures:
    def test_all_features(self):
        cover = _make_cover(position=True, control=True, tiltpos=True)
        flags = cover.supported_features
        assert flags & CoverEntityFeature.SET_POSITION
        assert flags & CoverEntityFeature.STOP
        assert flags & CoverEntityFeature.OPEN
        assert flags & CoverEntityFeature.CLOSE
        assert flags & CoverEntityFeature.SET_TILT_POSITION

    def test_position_only(self):
        cover = _make_cover(position=True, control=False, tiltpos=False, action=False)
        flags = cover.supported_features
        assert flags & CoverEntityFeature.SET_POSITION
        assert not (flags & CoverEntityFeature.STOP)

    def test_no_stop(self):
        cover = _make_cover(control=True, control_values=["open", "close"])
        flags = cover.supported_features
        assert not (flags & CoverEntityFeature.STOP)
        assert flags & CoverEntityFeature.OPEN
        assert flags & CoverEntityFeature.CLOSE


class TestDeviceClass:
    def test_valid_class(self):
        cover = _make_cover()
        cover._config.device_class = "garage"
        assert cover.device_class == CoverDeviceClass.GARAGE

    def test_none_class(self):
        cover = _make_cover()
        cover._config.device_class = None
        assert cover.device_class is None

    def test_invalid_class_logs_warning(self):
        cover = _make_cover()
        cover._config.device_class = "invalid_class"
        with patch("custom_components.tuya_local.cover._LOGGER") as mock_logger:
            result = cover.device_class
            assert result is None
            mock_logger.warning.assert_called_once()


class TestStateToPercent:
    def test_opened(self):
        cover = _make_cover()
        assert cover._state_to_percent("opened") == 100

    def test_closed(self):
        cover = _make_cover()
        assert cover._state_to_percent("closed") == 0

    def test_other(self):
        cover = _make_cover()
        assert cover._state_to_percent("opening") == 50


class TestCurrentCoverPosition:
    def test_from_currentpos_dp(self):
        cover = _make_cover(currentpos=True)
        cover._currentpos_dp.get_value.return_value = 75
        assert cover.current_cover_position == 75

    def test_currentpos_none_falls_through(self):
        cover = _make_cover(currentpos=True, action=True)
        cover._currentpos_dp.get_value.return_value = None
        cover._action_dp.get_value.return_value = "opened"
        assert cover.current_cover_position == 100

    def test_from_open_dp_true(self):
        cover = _make_cover(
            currentpos=False, open_dp=True, action=False, control=False, position=False
        )
        cover._open_dp.get_value.return_value = True
        assert cover.current_cover_position == 100

    def test_from_open_dp_false(self):
        cover = _make_cover(
            currentpos=False, open_dp=True, action=False, control=False, position=False
        )
        cover._open_dp.get_value.return_value = False
        assert cover.current_cover_position == 0

    def test_from_action_dp(self):
        cover = _make_cover(
            currentpos=False, open_dp=False, action=True, control=False, position=False
        )
        cover._action_dp.get_value.return_value = "closed"
        assert cover.current_cover_position == 0

    def test_from_position_dp(self):
        cover = _make_cover(
            currentpos=False, open_dp=False, action=False, control=False, position=True
        )
        cover._position_dp.get_value.return_value = 42
        assert cover.current_cover_position == 42

    def test_none_when_no_dps(self):
        cover = _make_cover(
            currentpos=False,
            open_dp=False,
            action=False,
            control=False,
            position=False,
        )
        assert cover.current_cover_position is None


class TestCurrentCoverTiltPosition:
    def test_with_range(self):
        cover = _make_cover(tiltpos=True)
        cover._tiltpos_dp.range.return_value = (0, 255)
        cover._tiltpos_dp.get_value.return_value = 128
        pos = cover.current_cover_tilt_position
        assert pos is not None
        assert 0 <= pos <= 100

    def test_without_range(self):
        cover = _make_cover(tiltpos=True)
        cover._tiltpos_dp.range.return_value = None
        cover._tiltpos_dp.get_value.return_value = 50
        assert cover.current_cover_tilt_position == 50

    def test_none_without_dp(self):
        cover = _make_cover(tiltpos=False)
        assert cover.current_cover_tilt_position is None


class TestCurrentState:
    def test_action_opening(self):
        cover = _make_cover()
        cover._action_dp.get_value.return_value = "opening"
        assert cover._current_state == "opening"

    def test_action_closing(self):
        cover = _make_cover()
        cover._action_dp.get_value.return_value = "closing"
        assert cover._current_state == "closing"

    def test_action_opened(self):
        cover = _make_cover()
        cover._action_dp.get_value.return_value = "opened"
        assert cover._current_state == "opened"

    def test_action_closed(self):
        cover = _make_cover()
        cover._action_dp.get_value.return_value = "closed"
        assert cover._current_state == "closed"

    def test_action_unknown_falls_to_position(self):
        cover = _make_cover(currentpos=True, action=True)
        cover._action_dp.get_value.return_value = "idle"
        cover._currentpos_dp.get_value.return_value = 0
        assert cover._current_state == "closed"

    def test_position_low_is_closed(self):
        cover = _make_cover(action=False, currentpos=True)
        cover._currentpos_dp.get_value.return_value = 3
        assert cover._current_state == "closed"

    def test_position_high_is_opened(self):
        cover = _make_cover(action=False, currentpos=True)
        cover._currentpos_dp.get_value.return_value = 98
        assert cover._current_state == "opened"

    def test_mid_position_with_setpos_match_is_opened(self):
        cover = _make_cover(action=False, currentpos=True, position=True)
        cover._currentpos_dp.get_value.return_value = 50
        cover._position_dp.get_value.return_value = 50
        assert cover._current_state == "opened"

    def test_mid_position_near_setpos_is_opened(self):
        cover = _make_cover(action=False, currentpos=True, position=True)
        cover._currentpos_dp.get_value.return_value = 49
        cover._position_dp.get_value.return_value = 50
        assert cover._current_state == "opened"

    def test_mid_position_with_open_cmd_is_opening(self):
        cover = _make_cover(action=False, currentpos=True, position=True, control=True)
        cover._currentpos_dp.get_value.return_value = 50
        cover._position_dp.get_value.return_value = 80
        cover._control_dp.get_value.return_value = "open"
        assert cover._current_state == "opening"

    def test_mid_position_with_close_cmd_is_closing(self):
        cover = _make_cover(action=False, currentpos=True, position=True, control=True)
        cover._currentpos_dp.get_value.return_value = 50
        cover._position_dp.get_value.return_value = 20
        cover._control_dp.get_value.return_value = "close"
        assert cover._current_state == "closing"

    def test_none_when_no_position(self):
        cover = _make_cover(
            action=False,
            currentpos=False,
            position=False,
            control=False,
            open_dp=False,
        )
        assert cover._current_state is None


class TestIsOpeningClosingClosed:
    def test_is_opening_true(self):
        cover = _make_cover()
        cover._action_dp.get_value.return_value = "opening"
        assert cover.is_opening is True

    def test_is_opening_false(self):
        cover = _make_cover()
        cover._action_dp.get_value.return_value = "closed"
        assert cover.is_opening is False

    def test_is_opening_none(self):
        cover = _make_cover(
            action=False,
            currentpos=False,
            position=False,
            control=False,
            open_dp=False,
        )
        assert cover.is_opening is None

    def test_is_closing_true(self):
        cover = _make_cover()
        cover._action_dp.get_value.return_value = "closing"
        assert cover.is_closing is True

    def test_is_closing_false(self):
        cover = _make_cover()
        cover._action_dp.get_value.return_value = "opened"
        assert cover.is_closing is False

    def test_is_closing_none(self):
        cover = _make_cover(
            action=False,
            currentpos=False,
            position=False,
            control=False,
            open_dp=False,
        )
        assert cover.is_closing is None

    def test_is_closed_true(self):
        cover = _make_cover()
        cover._action_dp.get_value.return_value = "closed"
        assert cover.is_closed is True

    def test_is_closed_false(self):
        cover = _make_cover()
        cover._action_dp.get_value.return_value = "opened"
        assert cover.is_closed is False

    def test_is_closed_none(self):
        cover = _make_cover(
            action=False,
            currentpos=False,
            position=False,
            control=False,
            open_dp=False,
        )
        assert cover.is_closed is None


class TestAsyncOpenCover:
    @pytest.mark.asyncio
    async def test_open_with_control(self):
        cover = _make_cover()
        cover._control_dp.async_set_value = AsyncMock()
        await cover.async_open_cover()
        cover._control_dp.async_set_value.assert_awaited_once_with(
            cover._device, "open"
        )

    @pytest.mark.asyncio
    async def test_open_with_position(self):
        cover = _make_cover(control=False)
        cover._position_dp.async_set_value = AsyncMock()
        await cover.async_open_cover()
        cover._position_dp.async_set_value.assert_awaited_once_with(cover._device, 100)

    @pytest.mark.asyncio
    async def test_open_raises_not_implemented(self):
        cover = _make_cover(control=False, position=False, action=False)
        with pytest.raises(NotImplementedError):
            await cover.async_open_cover()


class TestAsyncCloseCover:
    @pytest.mark.asyncio
    async def test_close_with_control(self):
        cover = _make_cover()
        cover._control_dp.async_set_value = AsyncMock()
        await cover.async_close_cover()
        cover._control_dp.async_set_value.assert_awaited_once_with(
            cover._device, "close"
        )

    @pytest.mark.asyncio
    async def test_close_with_position(self):
        cover = _make_cover(control=False)
        cover._position_dp.async_set_value = AsyncMock()
        await cover.async_close_cover()
        cover._position_dp.async_set_value.assert_awaited_once_with(cover._device, 0)

    @pytest.mark.asyncio
    async def test_close_raises_not_implemented(self):
        cover = _make_cover(control=False, position=False, action=False)
        with pytest.raises(NotImplementedError):
            await cover.async_close_cover()


class TestAsyncSetCoverPosition:
    @pytest.mark.asyncio
    async def test_set_position(self):
        cover = _make_cover()
        cover._position_dp.async_set_value = AsyncMock()
        await cover.async_set_cover_position(position=50)
        cover._position_dp.async_set_value.assert_awaited_once_with(cover._device, 50)

    @pytest.mark.asyncio
    async def test_set_position_none_raises(self):
        cover = _make_cover()
        with pytest.raises(AttributeError):
            await cover.async_set_cover_position(position=None)

    @pytest.mark.asyncio
    async def test_set_position_no_dp_raises(self):
        cover = _make_cover(position=False, action=False)
        with pytest.raises(NotImplementedError):
            await cover.async_set_cover_position(position=50)


class TestAsyncSetCoverTiltPosition:
    @pytest.mark.asyncio
    async def test_tilt_with_fixed_values(self):
        cover = _make_cover(tiltpos=True)
        cover._tiltpos_dp.values.return_value = [0, 50, 100]
        cover._tiltpos_dp.range.return_value = None
        cover._tiltpos_dp.async_set_value = AsyncMock()
        await cover.async_set_cover_tilt_position(tilt_position=60)
        # Should snap to 50 (closest)
        cover._tiltpos_dp.async_set_value.assert_awaited_once_with(cover._device, 50)

    @pytest.mark.asyncio
    async def test_tilt_with_range(self):
        cover = _make_cover(tiltpos=True)
        cover._tiltpos_dp.values.return_value = []
        cover._tiltpos_dp.range.return_value = (0, 255)
        cover._tiltpos_dp.async_set_value = AsyncMock()
        await cover.async_set_cover_tilt_position(tilt_position=50)
        cover._tiltpos_dp.async_set_value.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_tilt_raw(self):
        cover = _make_cover(tiltpos=True)
        cover._tiltpos_dp.values.return_value = []
        cover._tiltpos_dp.range.return_value = None
        cover._tiltpos_dp.async_set_value = AsyncMock()
        await cover.async_set_cover_tilt_position(tilt_position=75)
        cover._tiltpos_dp.async_set_value.assert_awaited_once_with(cover._device, 75)

    @pytest.mark.asyncio
    async def test_tilt_no_dp_raises(self):
        cover = _make_cover(tiltpos=False)
        with pytest.raises(NotImplementedError):
            await cover.async_set_cover_tilt_position(tilt_position=50)


class TestAsyncStopCover:
    @pytest.mark.asyncio
    async def test_stop_with_control(self):
        cover = _make_cover()
        cover._control_dp.async_set_value = AsyncMock()
        await cover.async_stop_cover()
        cover._control_dp.async_set_value.assert_awaited_once_with(
            cover._device, "stop"
        )

    @pytest.mark.asyncio
    async def test_stop_raises_not_implemented(self):
        cover = _make_cover(control=False, position=False, action=False)
        with pytest.raises(NotImplementedError):
            await cover.async_stop_cover()
