"""Tets for main API."""
from datetime import datetime
from typing import Callable, Union
from unittest.mock import patch

import httpx
import pytest
import respx
from typing_extensions import Final

from ismartgate import (
    AbstractGateApi,
    GogoGate2Api,
    GogoGate2ApiCipher,
    ISmartGateApi,
    ISmartGateApiCipher,
)
from ismartgate.common import (
    ApiError,
    DoorStatus,
    InvalidDoorException,
    TransitionDoorStatus,
    get_door_by_id,
)
from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode

from .common import MockGogoGate2Server, MockISmartGateMiniServer, MockISmartGateServer

ApiType = Union[GogoGate2Api, ISmartGateApi]
ServerType = Union[MockGogoGate2Server, MockISmartGateServer, MockISmartGateMiniServer]
ApiGenerator = Callable[[str, str, str], ApiType]
ServerGenerator = Callable[..., ServerType]


def test_ismartgate_token() -> None:
    """Test token generation."""

    assert (
        ISmartGateApiCipher("admin", "password").token
        == "f7001ecfe4d09ea0e58cb09058ba11ffe3ea36f0"
    )
    assert (
        ISmartGateApiCipher("admin2", "password2").token
        == "6669032e03454ecd1c6ea59abe49c6ed4303ff63"
    )


def test_ismartgate_encrypt_decrypt() -> None:
    """Test encrypt/decrypt."""
    cipher: Final = ISmartGateApiCipher("admin", "password")

    # Test encrypted contents matches exactly and can be decrypted.
    enc: Final = cipher.encrypt(
        '["admin", "notRealPassword", "info", "", ""]', init_vector="497c04879e0d26af"
    )
    assert (
        enc
        == "497c04879e0d26afxuTQ0lB1Rd0c0G/l6Tiw+YCjnN9oG26d3I5IyGQpvkcpJ9l2aHDcTdquB0RnkWgi"
    )
    assert cipher.decrypt(enc) == '["admin", "notRealPassword", "info", "", ""]'

    # Test with generated initialization vector.
    assert cipher.decrypt(cipher.encrypt("Hello World")) == "Hello World"

    # Test initialization vector padding.
    assert (
        cipher.decrypt(cipher.encrypt("Hello World", init_vector="A")) == "Hello World"
    )


def test_gogogate2_cipher() -> None:
    """Test encrypt/decrypt."""
    cipher: Final = GogoGate2ApiCipher()

    # Test encrypted contents matches exactly and can be decrypted.
    enc: Final = cipher.encrypt(
        '["admin", "notRealPassword", "info", "", ""]', init_vector="497c04879e0d26af"
    )
    assert (
        enc
        == "497c04879e0d26afhnGLw5SJBnC0qUqNFHYTBgzIGsTyUF96EyZMyAHIUrLiQobociSp4fkBhf1sWSWc"
    )
    assert cipher.decrypt(enc) == '["admin", "notRealPassword", "info", "", ""]'

    # Test with generated initialization vector.
    assert cipher.decrypt(cipher.encrypt("Hello World")) == "Hello World"

    # Test initialization vector padding.
    assert (
        cipher.decrypt(cipher.encrypt("Hello World", init_vector="A")) == "Hello World"
    )


@pytest.mark.parametrize(
    ("api_generator", "server_generator", "error_code"),
    (
        (
            GogoGate2Api,
            MockGogoGate2Server,
            GogoGate2ApiErrorCode.CREDENTIALS_INCORRECT.value,
        ),
        (
            ISmartGateApi,
            MockISmartGateServer,
            ISmartGateApiErrorCode.CREDENTIALS_INCORRECT.value,
        ),
        (
            ISmartGateApi,
            MockISmartGateMiniServer,
            ISmartGateApiErrorCode.CREDENTIALS_INCORRECT.value,
        ),
    ),
)
@pytest.mark.asyncio
@respx.mock
async def test_api_invalid_credentials(
    api_generator: ApiGenerator, server_generator: ServerGenerator, error_code: int
) -> None:
    """Test invalid credentials error."""
    # Incorrect username/password.
    api: Final = api_generator("device1", "fakeuser", "fakepassword")
    server_generator(api, username="fakeuser1", password="fakepassword2")
    with pytest.raises(ApiError) as exinfo:
        await api.async_info()
    assert exinfo.value.code == error_code


@pytest.mark.parametrize(
    ("api_generator", "server_generator"),
    (
        (GogoGate2Api, MockGogoGate2Server),
        (ISmartGateApi, MockISmartGateServer),
        (ISmartGateApi, MockISmartGateMiniServer),
    ),
)
@pytest.mark.asyncio
@respx.mock
async def test_activate(
    api_generator: ApiGenerator, server_generator: ServerGenerator
) -> None:
    """Test activate."""
    api: Final = api_generator("device1", "fakeuser", "fakepassword")
    server_generator(api)

    response = await api.async_activate(1)
    assert response
    assert response.result


@pytest.mark.parametrize(
    ("api_generator", "server_generator"),
    (
        (GogoGate2Api, MockGogoGate2Server),
        (ISmartGateApi, MockISmartGateServer),
    ),
)
@pytest.mark.asyncio
@respx.mock
# pylint: disable=too-many-statements
async def test_open_and_close_door(
    api_generator: ApiGenerator, server_generator: ServerGenerator
) -> None:
    """Test open and close door."""
    api: Final = api_generator("device1", "fakeuser", "fakepassword")
    server_generator(api)

    # Initial info.
    response = await api.async_info()
    door1 = get_door_by_id(1, response)
    door2 = get_door_by_id(2, response)
    door3 = get_door_by_id(3, response)
    assert door1
    assert door2
    assert door3
    assert door1.status == DoorStatus.CLOSED
    assert door2.status == DoorStatus.OPENED
    assert door3.status == DoorStatus.UNDEFINED

    # Nothing changes because door is already closed.
    assert await api.async_close_door(1) is False
    response = await api.async_info()
    door1 = get_door_by_id(1, response)
    door2 = get_door_by_id(2, response)
    door3 = get_door_by_id(3, response)
    assert door1
    assert door2
    assert door3
    assert door1.status == DoorStatus.CLOSED
    assert door2.status == DoorStatus.OPENED
    assert door3.status == DoorStatus.UNDEFINED

    # Open a door.
    assert await api.async_open_door(1) is True
    response = await api.async_info()
    door1 = get_door_by_id(1, response)
    door2 = get_door_by_id(2, response)
    door3 = get_door_by_id(3, response)
    assert door1
    assert door2
    assert door3
    assert door1.status == DoorStatus.OPENED
    assert door2.status == DoorStatus.OPENED
    assert door3.status == DoorStatus.UNDEFINED

    # Close a door.
    assert await api.async_close_door(2) is True
    response = await api.async_info()
    door1 = get_door_by_id(1, response)
    door2 = get_door_by_id(2, response)
    door3 = get_door_by_id(3, response)
    assert door1
    assert door2
    assert door3
    assert door1.status == DoorStatus.OPENED
    assert door2.status == DoorStatus.CLOSED
    assert door3.status == DoorStatus.UNDEFINED

    # No change for already closed door.
    assert await api.async_close_door(2) is False
    response = await api.async_info()
    door1 = get_door_by_id(1, response)
    door2 = get_door_by_id(2, response)
    door3 = get_door_by_id(3, response)
    assert door1
    assert door2
    assert door3
    assert door1.status == DoorStatus.OPENED
    assert door2.status == DoorStatus.CLOSED
    assert door3.status == DoorStatus.UNDEFINED

    # No change for unknown door.
    assert await api.async_close_door(8) is False
    response = await api.async_info()
    door1 = get_door_by_id(1, response)
    door2 = get_door_by_id(2, response)
    door3 = get_door_by_id(3, response)
    assert door1
    assert door2
    assert door3
    assert door1.status == DoorStatus.OPENED
    assert door2.status == DoorStatus.CLOSED
    assert door3.status == DoorStatus.UNDEFINED

    # No change for unsupported status.
    # pylint: disable=protected-access
    assert await api._async_set_door_status(1, DoorStatus.UNDEFINED) is False
    response = await api.async_info()
    door1 = get_door_by_id(1, response)
    door2 = get_door_by_id(2, response)
    door3 = get_door_by_id(3, response)
    assert door1
    assert door2
    assert door3
    assert door1.status == DoorStatus.OPENED
    assert door2.status == DoorStatus.CLOSED
    assert door3.status == DoorStatus.UNDEFINED


@pytest.mark.parametrize(
    ("api_generator", "server_generator", "true_value"),
    (
        (GogoGate2Api, MockGogoGate2Server, "1"),
        (ISmartGateApi, MockISmartGateServer, "yes"),
    ),
)
@pytest.mark.asyncio
@respx.mock
# pylint: disable=too-many-statements
async def test_remoteaccess(
    api_generator: ApiGenerator, server_generator: ServerGenerator, true_value: str
) -> None:
    """Test open and close door."""
    api: Final = api_generator("device1", "fakeuser", "fakepassword")
    server: Final = server_generator(api)

    server.set_info_value("remoteaccessenabled", "false")
    assert not (await api.async_info()).remoteaccessenabled
    server.set_info_value("remoteaccessenabled", "no")
    assert not (await api.async_info()).remoteaccessenabled
    server.set_info_value("remoteaccessenabled", "0")
    assert not (await api.async_info()).remoteaccessenabled

    server.set_info_value("remoteaccessenabled", true_value)
    assert (await api.async_info()).remoteaccessenabled
    server.set_info_value("remoteaccessenabled", true_value.upper())
    assert (await api.async_info()).remoteaccessenabled
    server.set_info_value("remoteaccessenabled", true_value.lower())
    assert (await api.async_info()).remoteaccessenabled


@pytest.mark.parametrize(
    ("api_generator", "server_generator"),
    (
        (GogoGate2Api, MockGogoGate2Server),
        (ISmartGateApi, MockISmartGateServer),
    ),
)
@pytest.mark.asyncio
@respx.mock
# pylint: disable=too-many-statements
async def test_sensor_temperature_and_voltage(
    api_generator: ApiGenerator, server_generator: ServerGenerator
) -> None:
    """Test open and close door."""
    api: Final = api_generator("device1", "fakeuser", "fakepassword")
    server_generator(api)

    # Initial info.
    response: Final = await api.async_info()
    assert response.door1.temperature == 16.3
    assert response.door1.voltage == 40

    assert response.door2.temperature is None
    assert response.door2.voltage == 40

    assert response.door3.temperature == 16.3
    assert response.door3.voltage is None


@pytest.mark.asyncio
@respx.mock
# pylint: disable=too-many-statements
async def test_empty_apicode() -> None:
    """Test open and close door."""
    api: Final = ISmartGateApi("device1", "fakeuser", "fakepassword")
    MockISmartGateServer(api, "")

    # Initial info.
    response: Final = await api.async_info()
    assert response.door1.apicode == ""
    assert response.door2.apicode == "0"
    assert response.door3.apicode == "0"


@pytest.mark.parametrize(
    ("api_generator", "server_generator"),
    (
        (GogoGate2Api, MockGogoGate2Server),
        (ISmartGateApi, MockISmartGateServer),
    ),
)
@pytest.mark.asyncio
@respx.mock
# pylint: disable=too-many-statements
async def test_transitional_door_statuses(
    api_generator: ApiGenerator, server_generator: ServerGenerator
) -> None:
    """Test open and close door."""
    with patch("ismartgate.datetime") as datetime_mock:
        api: Final = api_generator("device1", "fakeuser", "fakepassword")
        mock_server: Final = server_generator(api)

        # Test current status.
        datetime_mock.utcnow.side_effect = datetime.utcnow
        api._transition_door_status.clear()  # pylint: disable=protected-access
        info = await api.async_info()
        assert await api.async_get_door_statuses() == {
            1: DoorStatus.CLOSED,
            2: DoorStatus.OPENED,
        }
        assert await api.async_get_door_statuses(use_transitional_status=True) == {
            1: DoorStatus.CLOSED,
            2: DoorStatus.OPENED,
        }
        assert await api.async_get_door_statuses(use_transitional_status=False) == {
            1: DoorStatus.CLOSED,
            2: DoorStatus.OPENED,
        }
        assert api.async_get_door_statuses_from_info(info) == {
            1: DoorStatus.CLOSED,
            2: DoorStatus.OPENED,
        }
        assert api.async_get_door_statuses_from_info(
            info, use_transitional_status=True
        ) == {1: DoorStatus.CLOSED, 2: DoorStatus.OPENED}
        assert api.async_get_door_statuses_from_info(
            info, use_transitional_status=False
        ) == {1: DoorStatus.CLOSED, 2: DoorStatus.OPENED}

        # Test door is in the process of opening.
        datetime_mock.utcnow.side_effect = datetime.utcnow
        api._transition_door_status.clear()  # pylint: disable=protected-access
        await api.async_open_door(1)
        mock_server.set_device_status(1, DoorStatus.CLOSED)
        info = await api.async_info()
        assert await api.async_get_door_statuses() == {
            1: TransitionDoorStatus.OPENING,
            2: DoorStatus.OPENED,
        }
        assert await api.async_get_door_statuses(use_transitional_status=True) == {
            1: TransitionDoorStatus.OPENING,
            2: DoorStatus.OPENED,
        }
        assert await api.async_get_door_statuses(use_transitional_status=False) == {
            1: DoorStatus.CLOSED,
            2: DoorStatus.OPENED,
        }
        assert api.async_get_door_statuses_from_info(info) == {
            1: TransitionDoorStatus.OPENING,
            2: DoorStatus.OPENED,
        }
        assert api.async_get_door_statuses_from_info(
            info, use_transitional_status=True
        ) == {1: TransitionDoorStatus.OPENING, 2: DoorStatus.OPENED}
        assert api.async_get_door_statuses_from_info(
            info, use_transitional_status=False
        ) == {1: DoorStatus.CLOSED, 2: DoorStatus.OPENED}

        # Door is open before the transitional cache timeout.
        datetime_mock.utcnow.side_effect = datetime.utcnow
        api._transition_door_status.clear()  # pylint: disable=protected-access
        await api.async_open_door(1)
        info = await api.async_info()
        assert await api.async_get_door_statuses() == {
            1: DoorStatus.OPENED,
            2: DoorStatus.OPENED,
        }
        assert await api.async_get_door_statuses(use_transitional_status=True) == {
            1: DoorStatus.OPENED,
            2: DoorStatus.OPENED,
        }
        assert await api.async_get_door_statuses(use_transitional_status=False) == {
            1: DoorStatus.OPENED,
            2: DoorStatus.OPENED,
        }
        assert api.async_get_door_statuses_from_info(info) == {
            1: DoorStatus.OPENED,
            2: DoorStatus.OPENED,
        }
        assert api.async_get_door_statuses_from_info(
            info, use_transitional_status=True
        ) == {1: DoorStatus.OPENED, 2: DoorStatus.OPENED}
        assert api.async_get_door_statuses_from_info(
            info, use_transitional_status=False
        ) == {1: DoorStatus.OPENED, 2: DoorStatus.OPENED}

        # Door remains closed after the transitional cache timeout.
        datetime_mock.utcnow.side_effect = datetime.utcnow
        api._transition_door_status.clear()  # pylint: disable=protected-access
        mock_server.set_device_status(1, DoorStatus.CLOSED)
        await api.async_open_door(1)
        mock_server.set_device_status(1, DoorStatus.CLOSED)
        datetime_mock.utcnow.side_effect = (
            lambda: datetime.utcnow()
            + AbstractGateApi.DEFAULT_TRANSITION_STATUS_TIMEOUT
        )
        info = await api.async_info()
        assert await api.async_get_door_statuses() == {
            1: DoorStatus.CLOSED,
            2: DoorStatus.OPENED,
        }
        assert await api.async_get_door_statuses(use_transitional_status=True) == {
            1: DoorStatus.CLOSED,
            2: DoorStatus.OPENED,
        }
        assert await api.async_get_door_statuses(use_transitional_status=False) == {
            1: DoorStatus.CLOSED,
            2: DoorStatus.OPENED,
        }
        assert api.async_get_door_statuses_from_info(info) == {
            1: DoorStatus.CLOSED,
            2: DoorStatus.OPENED,
        }
        assert api.async_get_door_statuses_from_info(
            info, use_transitional_status=True
        ) == {1: DoorStatus.CLOSED, 2: DoorStatus.OPENED}
        assert api.async_get_door_statuses_from_info(
            info, use_transitional_status=False
        ) == {1: DoorStatus.CLOSED, 2: DoorStatus.OPENED}


@pytest.mark.parametrize(
    ("api_generator", "server_generator"),
    ((ISmartGateApi, MockISmartGateServer), (ISmartGateApi, MockISmartGateMiniServer)),
)
@pytest.mark.asyncio
@respx.mock
# pylint: disable=too-many-statements
async def test_ismartgate_activate_invalid_door(
    api_generator: ApiGenerator, server_generator: ServerGenerator
) -> None:
    """Test invalid door exception when calling activate for ismartgate."""
    api: Final = api_generator("device1", "fakeuser", "fakepassword")
    server_generator(api)

    with pytest.raises(InvalidDoorException):
        await api.async_activate(5)


@pytest.mark.parametrize(
    ("api_generator", "server_generator"),
    (
        (GogoGate2Api, MockGogoGate2Server),
        (ISmartGateApi, MockISmartGateServer),
        (ISmartGateApi, MockISmartGateMiniServer),
    ),
)
@pytest.mark.asyncio
@respx.mock
# pylint: disable=too-many-statements
async def test_httpx_client(
    api_generator: ApiGenerator, server_generator: ServerGenerator
) -> None:
    """Verify we can accept a shared httpx AsyncClient."""
    httpx_async_client: httpx.AsyncClient = httpx.AsyncClient()
    api: Final = api_generator(  # type: ignore
        "device1", "fakeuser", "fakepassword", httpx_async_client=httpx_async_client
    )
    server_generator(api)

    assert await api.async_info()
