"""BleBox cover entities tests."""

import json

import pytest

from blebox_uniapi.box_types import get_latest_api_level
from blebox_uniapi import error
from blebox_uniapi.cover import Shutter, ShutterBoxControlType, UnifiedCoverType

from .conftest import CommonEntity, DefaultBoxTest, future_date, jmerge

# TODO: remove
ATTR_POSITION = "ATTR_POSITION"
DEVICE_CLASS_DOOR = "DEVICE_CLASS_DOOR"
DEVICE_CLASS_SHUTTER = "DEVICE_CLASS_SHUTTER"
STATE_CLOSED = "STATE_CLOSED"
STATE_CLOSING = "STATE_CLOSING"
STATE_OPEN = "STATE_OPEN"
STATE_OPENING = "STATE_OPENING"

SUPPORT_OPEN = 1
SUPPORT_CLOSE = 2
SUPPORT_SET_POSITION = 4
SUPPORT_STOP = 8


def patch_version(apiLevel):
    """Helper function for generate a patch for a JSON state fixture."""
    return f"""{{ "device": {{ "apiLevel": {apiLevel} }} }}"""


class BleBoxCoverEntity(CommonEntity):
    """Representation of a BleBox cover feature."""

    @property
    def state(self):
        """Return the equivalent HA cover state."""
        states = {
            None: None,
            0: STATE_CLOSING,  # moving down
            1: STATE_OPENING,  # moving up
            2: STATE_OPEN,  # manually stopped
            3: STATE_CLOSED,  # lower limit
            4: STATE_OPEN,  # upper limit / open
            # gateController
            5: STATE_OPEN,  # overload
            6: STATE_OPEN,  # motor failure
            # 7: not used
            8: STATE_OPEN,  # safety stop
        }

        return states[self._feature.state]

    @property
    def device_class(self):
        """Return the device class."""
        types = {
            "shutter": DEVICE_CLASS_SHUTTER,
            "gatebox": DEVICE_CLASS_DOOR,
            # "gateboxB": DEVICE_CLASS_DOOR,
            "gate": DEVICE_CLASS_DOOR,
        }
        return types[self._feature.device_class]

    # TODO: does changing this at runtime really work?
    @property
    def supported_features(self):
        """Return the supported cover features."""
        position = SUPPORT_SET_POSITION if self._feature.is_slider else 0
        stop = SUPPORT_STOP if self._feature.has_stop else 0

        return position | stop | SUPPORT_OPEN | SUPPORT_CLOSE

    @property
    def current_cover_position(self):
        """Return the current cover position."""
        current = self._invert_position(self._feature.current)
        return current if current else None

    @property
    def is_opening(self):
        """Return whether cover is opening."""
        return self._is_state(STATE_OPENING)

    @property
    def is_closing(self):
        """Return whether cover is closing."""
        return self._is_state(STATE_CLOSING)

    @property
    def is_closed(self):
        """Return whether cover is closed."""
        return self._is_state(STATE_CLOSED)

    async def async_open_cover(self, **kwargs):
        """Open the cover position."""
        await self._feature.async_open()

    async def async_close_cover(self, **kwargs):
        """Close the cover position."""
        await self._feature.async_close()

    async def async_set_cover_position(self, **kwargs):
        """Set the cover position."""
        value = kwargs[ATTR_POSITION]
        await self._feature.async_set_position(self._invert_position(value))

    async def async_stop_cover(self, **kwargs):
        """Stop the cover."""
        await self._feature.async_stop()

    def _is_state(self, state_name):
        value = self.state
        return None if value is None else value == state_name

    def _invert_position(self, position):
        # NOTE: in BleBox, 100% means 'closed'
        return None if position is None else 100 - position


class CoverTest(DefaultBoxTest):
    """Shared test helpers for Cover tests."""

    DEVCLASS = "covers"
    ENTITY_CLASS = BleBoxCoverEntity

    # TODO: refactor more
    def assert_state(self, entity, state):
        """Assert that cover state is correct."""
        assert entity.state == state

        opening, closing, closed = {
            None: [None, None, None],
            STATE_OPEN: [False, False, False],
            STATE_OPENING: [True, False, False],
            STATE_CLOSING: [False, True, False],
            STATE_CLOSED: [False, False, True],
        }[state]

        assert entity.is_opening is opening
        assert entity.is_closing is closing
        assert entity.is_closed is closed


class TestShutterAttributes:
    """Unit tests for Shutter attribute properties based on control type."""

    def test_is_calibrated_false(self):
        assert Shutter(1, False).is_calibrated is False

    def test_is_calibrated_true(self):
        assert Shutter(1, True).is_calibrated is True

    @pytest.mark.parametrize(
        "control_type",
        [
            ShutterBoxControlType.TILT_SHUTTER_90,
            ShutterBoxControlType.TILT_SHUTTER_180,
            ShutterBoxControlType.TILT_ONLY,
            ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY,
            ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING,
        ],
    )
    def test_has_tilt_true(self, control_type):
        assert Shutter(control_type).has_tilt is True

    @pytest.mark.parametrize(
        "control_type",
        [
            ShutterBoxControlType.SEGMENTED_SHUTTER,
            ShutterBoxControlType.NO_CALIBRATION,
            ShutterBoxControlType.WINDOW_OPENER,
            ShutterBoxControlType.PERGOLA_ROOF,
            ShutterBoxControlType.CURTAIN,
        ],
    )
    def test_has_tilt_false(self, control_type):
        assert Shutter(control_type).has_tilt is False

    @pytest.mark.parametrize(
        "control_type",
        [
            ShutterBoxControlType.TILT_ONLY,
            ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY,
        ],
    )
    def test_tilt_only_true(self, control_type):
        assert Shutter(control_type).tilt_only is True

    @pytest.mark.parametrize(
        "control_type",
        [
            ShutterBoxControlType.TILT_SHUTTER_90,
            ShutterBoxControlType.TILT_SHUTTER_180,
            ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING,
            ShutterBoxControlType.SEGMENTED_SHUTTER,
        ],
    )
    def test_tilt_only_false(self, control_type):
        assert Shutter(control_type).tilt_only is False

    def test_is_tilt_180_true(self):
        assert Shutter(ShutterBoxControlType.TILT_SHUTTER_180).is_tilt_180 is True

    @pytest.mark.parametrize(
        "control_type",
        [
            ShutterBoxControlType.TILT_SHUTTER_90,
            ShutterBoxControlType.TILT_ONLY,
            ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING,
        ],
    )
    def test_is_tilt_180_false(self, control_type):
        assert Shutter(control_type).is_tilt_180 is False

    @pytest.mark.parametrize(
        "control_type",
        [
            ShutterBoxControlType.NO_CALIBRATION,
            ShutterBoxControlType.CURTAIN,
            ShutterBoxControlType.TILT_ONLY,
            ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY,
            ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING,
        ],
    )
    def test_is_slider_false_by_control_type(self, control_type):
        assert Shutter(control_type, is_calibrated=True).is_slider is False

    @pytest.mark.parametrize(
        "control_type",
        [
            ShutterBoxControlType.TILT_SHUTTER_90,
            ShutterBoxControlType.TILT_SHUTTER_180,
            ShutterBoxControlType.PERGOLA_ROOF,
            ShutterBoxControlType.SEGMENTED_SHUTTER,
        ],
    )
    def test_is_slider_false_when_not_calibrated(self, control_type):
        assert Shutter(control_type, is_calibrated=False).is_slider is False

    @pytest.mark.parametrize(
        "control_type",
        [
            ShutterBoxControlType.TILT_SHUTTER_90,
            ShutterBoxControlType.TILT_SHUTTER_180,
            ShutterBoxControlType.PERGOLA_ROOF,
            ShutterBoxControlType.SEGMENTED_SHUTTER,
        ],
    )
    def test_is_slider_true(self, control_type):
        assert Shutter(control_type, is_calibrated=True).is_slider is True

    @pytest.mark.parametrize(
        "control_type, expected",
        [
            (ShutterBoxControlType.ROOF_SEGMENTED_SHUTTER, UnifiedCoverType.SHUTTER),
            (ShutterBoxControlType.SERVOMOTOR, UnifiedCoverType.SHUTTER),
            (ShutterBoxControlType.CURTAIN_ONE_SIDED, UnifiedCoverType.CURTAIN),
            (ShutterBoxControlType.CURTAIN_TWO_SIDED, UnifiedCoverType.CURTAIN),
            (ShutterBoxControlType.TILT_ONLY, UnifiedCoverType.SHUTTER),
            (ShutterBoxControlType.PERGOLA_ROOF, UnifiedCoverType.AWNING),
            (ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY, UnifiedCoverType.AWNING),
            (ShutterBoxControlType.TILT_SHUTTER_180, UnifiedCoverType.SHUTTER),
            (
                ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING,
                UnifiedCoverType.SHUTTER,
            ),
        ],
    )
    def test_read_cover_type_new_types(self, control_type, expected):
        assert Shutter(control_type).read_cover_type(None, None, None) == expected


class TestShutter(CoverTest):
    """Tests for cover devices representing a BleBox ShutterBox."""

    DEV_INFO_PATH = "api/shutter/state"

    DEVICE_INFO = json.loads("""
    {
        "device": {
            "deviceName": "My shutter 1",
            "type": "shutterBox",
            "fv": "0.147",
            "hv": "0.7",
            "apiLevel": "20180604",
            "id": "2bee34e750b8",
            "ip": "172.0.0.1"
        }
    }
    """)

    DEVICE_EXTENDED_INFO_PATH = "/api/shutter/extended/state"
    DEVICE_EXTENDED_INFO = {
        "shutter": {
            "state": 2,
            "currentPos": {"position": 50, "tilt": 50},
            "desiredPos": {"position": 50, "tilt": 50},
            "favPos": {"position": 50, "tilt": 50},
            "controlType": 1,
            "calibrationParameters": {
                "isCalibrated": 1,
            },
        }
    }

    DEVICE_INFO_FUTURE = jmerge(DEVICE_INFO, patch_version(future_date()))
    DEVICE_INFO_LATEST = jmerge(
        DEVICE_INFO, patch_version(get_latest_api_level("shutterBox"))
    )
    DEVICE_INFO_UNSUPPORTED = jmerge(DEVICE_INFO, patch_version(20180603))

    DEVICE_INFO_UNSPECIFIED_API = json.loads("""
    {
        "device": {
            "deviceName": "My shutter 1",
            "type": "shutterBox",
            "fv": "0.147",
            "hv": "0.7",
            "id": "2bee34e750b8",
            "ip": "172.0.0.1"
        }
    }
    """)

    STATE_DEFAULT = json.loads("""
    {
        "shutter": {
            "state": 2,
            "currentPos": {
                "position": 34,
                "tilt": 3
            },
            "desiredPos": {
                "position": 78,
                "tilt": 97
            },
            "favPos": {
                "position": 13,
                "tilt": 17
            }
        }
    }
    """)

    def patch_state(state, current, desired):
        """Generate a patch for a JSON state fixture."""
        return f"""
        {{
            "shutter": {{
                "state": {state},
                "currentPos": {{ "position": {current} }},
                "desiredPos": {{ "position": {desired} }}
            }}
        }}
        """

    STATE_CLOSING = jmerge(STATE_DEFAULT, patch_state(0, 78, 100))
    STATE_CLOSED = jmerge(STATE_DEFAULT, patch_state(3, 100, 100))
    STATE_OPENING = jmerge(STATE_DEFAULT, patch_state(1, 34, 0))
    STATE_MINIMALLY_OPENING = jmerge(STATE_DEFAULT, patch_state(1, 97, 100))
    STATE_STOPPED = jmerge(STATE_DEFAULT, patch_state(2, 34, 100))
    STATE_UNKNOWN = jmerge(STATE_DEFAULT, patch_state(2, 34, -1))

    async def test_init(self, aioclient_mock):
        """Test cover default state."""

        await self.allow_get_info(aioclient_mock)
        entity = (await self.async_entities(aioclient_mock))[0]

        assert entity.name == "My shutter 1 (shutterBox#position)"
        assert entity.unique_id == "BleBox-shutterBox-2bee34e750b8-position"

        assert entity.device_class == DEVICE_CLASS_SHUTTER

        assert entity.supported_features & SUPPORT_OPEN
        assert entity.supported_features & SUPPORT_CLOSE
        assert entity.supported_features & SUPPORT_STOP

        assert entity._feature.is_calibrated is True
        assert entity.supported_features & SUPPORT_SET_POSITION
        assert entity.current_cover_position == 50
        assert entity._feature.is_position_inverted is True

        # TODO: tilt
        # assert entity.supported_features & SUPPORT_SET_TILT_POSITION
        # assert entity.current_cover_tilt_position == None

        self.assert_state(entity, STATE_OPEN)

    async def test_init_not_calibrated(self, aioclient_mock):
        """Test that is_calibrated is False when device reports isCalibrated=0."""
        original = self.DEVICE_EXTENDED_INFO
        self.DEVICE_EXTENDED_INFO = jmerge(
            original,
            '{"shutter": {"calibrationParameters": {"isCalibrated": 0}}}',
        )
        try:
            await self.allow_get_info(aioclient_mock)
            entity = (await self.async_entities(aioclient_mock))[0]
            assert entity._feature.is_calibrated is False
        finally:
            self.DEVICE_EXTENDED_INFO = original

    async def test_device_info(self, aioclient_mock):
        await self.allow_get_info(aioclient_mock, self.DEVICE_INFO)
        entity = (await self.async_entities(aioclient_mock))[0]
        assert entity.device_info["name"] == "My shutter 1"
        assert entity.device_info["mac"] == "2bee34e750b8"
        assert entity.device_info["manufacturer"] == "BleBox"
        assert entity.device_info["model"] == "shutterBox"
        assert entity.device_info["sw_version"] == "0.147"

    async def test_update(self, aioclient_mock):
        """Test cover updating."""

        entity = await self.updated(aioclient_mock, self.STATE_DEFAULT)

        assert entity.current_cover_position == 22  # 100 - 78
        self.assert_state(entity, STATE_OPEN)

        # TODO: tilt
        # assert entity.current_tilt_position == 0

    async def test_open(self, aioclient_mock):
        """Test cover opening."""

        entity = await self.updated(aioclient_mock, self.STATE_CLOSED)
        self.assert_state(entity, STATE_CLOSED)
        self.allow_get(aioclient_mock, "/s/u", self.STATE_OPENING)
        await entity.async_open_cover()
        self.assert_state(entity, STATE_OPENING)

    async def test_close(self, aioclient_mock):
        """Test cover closing."""

        entity = await self.updated(aioclient_mock, self.STATE_DEFAULT)
        self.assert_state(entity, STATE_OPEN)
        self.allow_get(aioclient_mock, "/s/d", self.STATE_CLOSING)
        await entity.async_close_cover()
        self.assert_state(entity, STATE_CLOSING)

    async def test_set_position(self, aioclient_mock):
        """Test cover position setting."""

        entity = await self.updated(aioclient_mock, self.STATE_CLOSED)
        self.assert_state(entity, STATE_CLOSED)
        self.allow_get(aioclient_mock, "/s/p/99", self.STATE_MINIMALLY_OPENING)
        await entity.async_set_cover_position(**{ATTR_POSITION: 1})  # almost closed
        self.assert_state(entity, STATE_OPENING)

    async def test_stop(self, aioclient_mock):
        """Test cover stopping."""

        entity = await self.updated(aioclient_mock, self.STATE_OPENING)
        self.assert_state(entity, STATE_OPENING)
        self.allow_get(aioclient_mock, "/s/s", self.STATE_STOPPED)
        await entity.async_stop_cover()
        self.assert_state(entity, STATE_OPEN)

    async def test_unkown_position(self, aioclient_mock):
        """Test handling cover at unknown position."""
        entity = await self.updated(aioclient_mock, self.STATE_UNKNOWN)
        self.assert_state(entity, STATE_OPEN)


class TestGateBox(CoverTest):
    """Tests for cover devices representing a BleBox gateBox."""

    DEV_INFO_PATH = "api/gate/state"

    DEVICE_INFO = json.loads("""
        {
            "deviceName": "My gate 1",
            "type": "gateBox",
            "fv": "0.176",
            "hv": "0.6",
            "id": "1afe34db9437",
            "ip": "192.168.1.11"
        }
        """)

    DEVICE_INFO_FUTURE = jmerge(DEVICE_INFO, f'{{ "apiLevel":"{future_date()}" }}')
    DEVICE_INFO_LATEST = jmerge(
        DEVICE_INFO, f'{{ "apiLevel":"{get_latest_api_level("gateBox")}" }}'
    )
    DEVICE_INFO_UNSUPPORTED = DEVICE_INFO

    DEVICE_INFO_UNSPECIFIED_API = None  # already handled as default case

    STATE_DEFAULT = json.loads("""
        {
            "currentPos": 50,
            "desiredPos": 50,
            "extraButtonType": 1,
            "extraButtonRelayNumber": 1,
            "extraButtonPulseTimeMs": 800,
            "extraButtonInvert": 1,
            "gateType": 0,
            "gateRelayNumber": 0,
            "gatePulseTimeMs": 800,
            "gateInvert": 0,
            "inputsType": 1,
            "openLimitSwitchInputNumber": 0,
            "closeLimitSwitchInputNumber": 1
        }
    """)

    STATE_CLOSED = jmerge(STATE_DEFAULT, '{ "currentPos": 0, "desiredPos": 0 }')
    STATE_OPENING = jmerge(STATE_DEFAULT, '{ "currentPos": 50, "desiredPos": 100 }')
    STATE_CLOSING = jmerge(STATE_DEFAULT, '{ "currentPos": 50, "desiredPos": 0 }')
    STATE_STOPPED = jmerge(STATE_DEFAULT, '{ "currentPos": 50, "desiredPos": 50 }')
    STATE_FULLY_OPENED = jmerge(
        STATE_DEFAULT, '{ "currentPos": 100, "desiredPos": 100 }'
    )

    STATE_OPENING_NO_STOP = jmerge(STATE_OPENING, '{ "extraButtonType": 3}')
    STATE_UNKNOWN = jmerge(STATE_DEFAULT, '{ "currentPos": -1, "desiredPos": 50 }')

    async def test_init(self, aioclient_mock):
        """Test cover default state."""

        await self.allow_get_info(aioclient_mock)
        entity = (await self.async_entities(aioclient_mock))[0]

        assert entity.name == "My gate 1 (gateBox#position)"
        assert entity.unique_id == "BleBox-gateBox-1afe34db9437-position"
        assert entity.device_class == DEVICE_CLASS_DOOR
        assert entity.supported_features & SUPPORT_OPEN
        assert entity.supported_features & SUPPORT_CLOSE

        # Not available since requires fetching state to detect
        assert not entity.supported_features & SUPPORT_STOP

        assert not entity.supported_features & SUPPORT_SET_POSITION
        assert entity.current_cover_position is None
        assert entity._feature.is_position_inverted is False
        self.assert_state(entity, None)

    async def test_device_info(self, aioclient_mock):
        await self.allow_get_info(aioclient_mock, self.DEVICE_INFO)
        entity = (await self.async_entities(aioclient_mock))[0]
        assert entity.device_info["name"] == "My gate 1"
        assert entity.device_info["mac"] == "1afe34db9437"
        assert entity.device_info["manufacturer"] == "BleBox"
        assert entity.device_info["model"] == "gateBox"
        assert entity.device_info["sw_version"] == "0.176"

    async def test_update(self, aioclient_mock):
        """Test cover updating."""

        entity = await self.updated(aioclient_mock, self.STATE_DEFAULT)

        assert entity.current_cover_position == 50  # 100 - 34
        self.assert_state(entity, STATE_OPEN)

    async def test_open(self, aioclient_mock):
        """Test cover opening."""

        entity = await self.updated(aioclient_mock, self.STATE_CLOSED)
        assert entity.state == STATE_CLOSED
        self.assert_state(entity, STATE_CLOSED)
        self.allow_get(aioclient_mock, "/s/p", self.STATE_OPENING)
        await entity.async_open_cover()
        self.assert_state(entity, STATE_OPENING)

    async def test_fully_opened(self, aioclient_mock):
        entity = await self.updated(aioclient_mock, self.STATE_FULLY_OPENED)
        assert entity.state == STATE_OPEN

    async def test_close(self, aioclient_mock):
        """Test cover closing."""

        entity = await self.updated(aioclient_mock, self.STATE_DEFAULT)
        self.assert_state(entity, STATE_OPEN)
        self.allow_get(aioclient_mock, "/s/p", self.STATE_CLOSING)
        await entity.async_close_cover()
        self.assert_state(entity, STATE_CLOSING)

    async def test_closed(self, aioclient_mock):
        """Test cover closed state."""

        entity = await self.updated(aioclient_mock, self.STATE_CLOSED)
        self.assert_state(entity, STATE_CLOSED)

    async def test_stop(self, aioclient_mock):
        """Test cover stopping."""

        entity = await self.updated(aioclient_mock, self.STATE_OPENING)
        self.assert_state(entity, STATE_OPENING)
        self.allow_get(aioclient_mock, "/s/s", self.STATE_STOPPED)
        await entity.async_stop_cover()
        self.assert_state(entity, STATE_OPEN)

    async def test_with_stop(self, aioclient_mock):
        """Test stop capability is available."""

        entity = await self.updated(aioclient_mock, self.STATE_OPENING)
        assert entity.supported_features & SUPPORT_STOP

    async def test_with_no_stop(self, aioclient_mock):
        """Test stop capability is not available."""

        entity = await self.updated(aioclient_mock, self.STATE_OPENING_NO_STOP)
        assert not entity.supported_features & SUPPORT_STOP

    async def test_stop_with_no_stop(self, aioclient_mock):
        """Test stop capability is not available."""

        entity = await self.updated(aioclient_mock, self.STATE_OPENING_NO_STOP)

        with pytest.raises(
            error.MisconfiguredDevice, match=r"second button not configured as 'stop'"
        ):
            await entity.async_stop_cover()

    async def test_set_position(self, aioclient_mock):
        """Test cover position setting."""

        entity = await self.updated(aioclient_mock, self.STATE_CLOSED)
        with pytest.raises(NotImplementedError):
            await entity.async_set_cover_position(**{ATTR_POSITION: 1})  # almost closed

    async def test_unkown_position(self, aioclient_mock):
        """Test handling cover at unknown position."""
        entity = await self.updated(aioclient_mock, self.STATE_UNKNOWN)
        self.assert_state(entity, None)


class TestGateBoxB(CoverTest):
    """Tests for cover devices representing a BleBox gateBoxB subgroup."""

    DEV_INFO_PATH = "state/extended"

    DEVICE_INFO = json.loads("""
        {
            "device": {
                "deviceName":"My gateBox 1",
                "type":"gateBox",
                "product":"gateBox",
                "hv":"9.1d",
                "fv":"0.1010",
                "universe":0,
                "apiLevel":"20200831",
                "id":"1afe34d27e4f",
                "ip":"192.168.4.1",
                "availableFv":null
            }
        }
        """)

    DEVICE_INFO_FUTURE = jmerge(DEVICE_INFO, patch_version(future_date()))
    DEVICE_INFO_LATEST = jmerge(
        DEVICE_INFO, patch_version(get_latest_api_level("gateBox"))
    )
    DEVICE_INFO_UNSUPPORTED = DEVICE_INFO

    DEVICE_INFO_UNSPECIFIED_API = None  # already handled as default case

    STATE_DEFAULT = json.loads("""
        {
            "gate": {
                "currentPos": 0,
                "openCloseMode": 0,
                "gateType": 1,
                "gatePulseTimeMs": 1500,
                "gateOutputState": 0,
                "extraButtonType": 1,
                "extraButtonPulseTimeMs": 1500,
                "extraButtonOutputState": 0,
                "inputsType": 0
            }
        }
        """)

    STATE_CLOSED = STATE_DEFAULT
    STATE_STOPPED = jmerge(STATE_DEFAULT, '{"gate": {"currentPos": 50 }}')
    STATE_FULLY_OPENED = jmerge(STATE_DEFAULT, '{"gate": {"currentPos": 100 }}')

    STATE_UNKNOWN = json.loads("""
        {
            "gate": {
                "currentPos": -1,
                "openCloseMode": 0,
                "gateType": 1,
                "gatePulseTimeMs": 1500,
                "gateOutputState": 0,
                "extraButtonType": 1,
                "extraButtonPulseTimeMs": 1500,
                "extraButtonOutputState": 0,
                "inputsType": 0
            }
        }
        """)

    async def test_init(self, aioclient_mock):
        """Test cover default state."""

        await self.allow_get_info(aioclient_mock)
        entity = (await self.async_entities(aioclient_mock))[0]

        assert entity.name == "My gateBox 1 (gateBox#position)"
        assert entity.unique_id == "BleBox-gateBox-1afe34d27e4f-position"
        assert entity.device_class == DEVICE_CLASS_DOOR
        assert entity.supported_features & SUPPORT_OPEN
        assert entity.supported_features & SUPPORT_CLOSE
        assert entity.current_cover_position is None
        assert entity._feature.is_position_inverted is False
        self.assert_state(entity, None)

    async def test_device_info(self, aioclient_mock):
        """Test device info state."""

        await self.allow_get_info(aioclient_mock, self.DEVICE_INFO)
        entity = (await self.async_entities(aioclient_mock))[0]
        # import pdb;pdb.set_trace()
        assert entity.device_info["name"] == "My gateBox 1"
        assert entity.device_info["mac"] == "1afe34d27e4f"
        assert entity.device_info["manufacturer"] == "BleBox"
        assert entity.device_info["model"] == "gateBox"
        assert entity.device_info["sw_version"] == "0.1010"

    async def test_fully_opened(self, aioclient_mock):
        """Test cover fully opened."""

        entity = await self.updated(aioclient_mock, self.STATE_FULLY_OPENED)
        assert entity.state == STATE_OPEN

    async def test_stop(self, aioclient_mock):
        """Test cover stopped."""

        entity = await self.updated(aioclient_mock, self.STATE_STOPPED)
        self.assert_state(entity, STATE_OPEN)

    async def test_closed(self, aioclient_mock):
        """Test cover closed."""

        entity = await self.updated(aioclient_mock, self.STATE_CLOSED)
        self.assert_state(entity, STATE_CLOSED)

    async def test_unkown_position(self, aioclient_mock):
        """Test handling cover at unknown position."""
        entity = await self.updated(aioclient_mock, self.STATE_UNKNOWN)
        self.assert_state(entity, None)


class TestGateController(CoverTest):
    """Tests for cover devices representing a BleBox gateController."""

    DEV_INFO_PATH = "api/gatecontroller/state"

    DEVICE_INFO = json.loads("""
    {
      "device": {
        "deviceName": "My gate controller 1",
        "type": "gateController",
        "apiLevel": "20180604",
        "fv": "1.390",
        "hv": "custom.2.6",
        "id": "0ff2ffaafe30db9437",
        "ip": "192.168.1.11"
      }
    }
    """)

    DEVICE_INFO_FUTURE = jmerge(DEVICE_INFO, patch_version(future_date()))
    DEVICE_INFO_LATEST = jmerge(
        DEVICE_INFO, patch_version(get_latest_api_level("gateController"))
    )
    DEVICE_INFO_UNSUPPORTED = jmerge(DEVICE_INFO, patch_version(20180603))

    # NOTE: can't happen with a real device
    DEVICE_INFO_UNSPECIFIED_API = json.loads("""
    {
      "device": {
        "deviceName": "My gate controller 1",
        "type": "gateController",
        "fv": "0.981",
        "hv": "custom.2.6",
        "id": "0ff2ffaafe30db9437",
        "ip": "192.168.1.11"
      }
    }
    """)

    STATE_DEFAULT = json.loads("""
    {
        "gateController": {
            "state": 2,
            "safety": {
                "eventReason": 0,
                "triggered": [ 0 ]
            },
            "currentPos": { "positions": [ 31 ] },
            "desiredPos": { "positions": [ 29 ] }
        }
    }
    """)

    def patch_state(state, current, desired):
        """Generate a patch for a JSON state fixture."""
        return f"""
        {{
            "gateController": {{
                "state": {state},
                "currentPos": {{ "positions": [ {current} ] }},
                "desiredPos": {{ "positions": [ {desired} ] }}
            }}
        }}
        """

    STATE_CLOSED = jmerge(STATE_DEFAULT, patch_state(3, 100, 100))
    STATE_OPENING = jmerge(STATE_DEFAULT, patch_state(1, 34, 0))
    STATE_CLOSING = jmerge(STATE_DEFAULT, patch_state(0, 78, 100))
    STATE_MINIMALLY_OPENING = jmerge(STATE_DEFAULT, patch_state(1, 97, 100))
    STATE_STOPPED = jmerge(STATE_DEFAULT, patch_state(2, 34, 100))

    async def test_init(self, aioclient_mock):
        """Test cover default state."""

        await self.allow_get_info(aioclient_mock)
        entity = (await self.async_entities(aioclient_mock))[0]

        assert entity.name == "My gate controller 1 (gateController#position)"
        assert entity.unique_id == "BleBox-gateController-0ff2ffaafe30db9437-position"

        assert entity.device_class == DEVICE_CLASS_DOOR

        assert entity.supported_features & SUPPORT_OPEN
        assert entity.supported_features & SUPPORT_CLOSE
        assert entity.supported_features & SUPPORT_STOP

        assert entity.supported_features & SUPPORT_SET_POSITION
        assert entity.current_cover_position is None
        assert entity._feature.is_position_inverted is True
        self.assert_state(entity, None)

    async def test_device_info(self, aioclient_mock):
        await self.allow_get_info(aioclient_mock, self.DEVICE_INFO)
        entity = (await self.async_entities(aioclient_mock))[0]
        assert entity.device_info["name"] == "My gate controller 1"
        assert entity.device_info["mac"] == "0ff2ffaafe30db9437"
        assert entity.device_info["manufacturer"] == "BleBox"
        assert entity.device_info["model"] == "gateController"
        assert entity.device_info["sw_version"] == "1.390"

    async def test_update(self, aioclient_mock):
        """Test cover updating."""

        entity = await self.updated(aioclient_mock, self.STATE_DEFAULT)

        assert entity.current_cover_position == 71  # 100 - 29
        self.assert_state(entity, STATE_OPEN)

    async def test_open(self, aioclient_mock):
        """Test cover opening."""

        entity = await self.updated(aioclient_mock, self.STATE_CLOSED)
        self.assert_state(entity, STATE_CLOSED)
        self.allow_get(aioclient_mock, "/s/o", self.STATE_OPENING)
        await entity.async_open_cover()
        self.assert_state(entity, STATE_OPENING)

    async def test_close(self, aioclient_mock):
        """Test cover closing."""

        entity = await self.updated(aioclient_mock, self.STATE_DEFAULT)
        self.assert_state(entity, STATE_OPEN)
        self.allow_get(aioclient_mock, "/s/c", self.STATE_CLOSING)
        await entity.async_close_cover()
        self.assert_state(entity, STATE_CLOSING)

    async def test_set_position(self, aioclient_mock):
        """Test cover position setting."""

        entity = await self.updated(aioclient_mock, self.STATE_CLOSED)
        self.assert_state(entity, STATE_CLOSED)
        self.allow_get(aioclient_mock, "/s/p/99", self.STATE_MINIMALLY_OPENING)
        await entity.async_set_cover_position(**{ATTR_POSITION: 1})  # almost closed
        self.assert_state(entity, STATE_OPENING)

    async def test_stop(self, aioclient_mock):
        """Test cover stopping."""

        entity = await self.updated(aioclient_mock, self.STATE_OPENING)
        self.assert_state(entity, STATE_OPENING)
        self.allow_get(aioclient_mock, "/s/s", self.STATE_STOPPED)
        await entity.async_stop_cover()
        self.assert_state(entity, STATE_OPEN)
