from datetime import datetime
from typing import Dict, Optional, Tuple

import pytest

from fastapi_users.authentication.strategy import RedisStrategy
from tests.conftest import IDType, UserModel


class RedisMock:
    store: Dict[str, Tuple[str, Optional[int]]]

    def __init__(self):
        self.store = {}

    async def get(self, key: str) -> Optional[str]:
        try:
            value, expiration = self.store[key]
            if expiration is not None and expiration < datetime.now().timestamp():
                return None
            return value
        except KeyError:
            return None

    async def set(self, key: str, value: str, ex: Optional[int] = None):
        expiration = None
        if ex is not None:
            expiration = int(datetime.now().timestamp() + ex)
        self.store[key] = (value, expiration)

    async def delete(self, key: str):
        try:
            del self.store[key]
        except KeyError:
            pass


@pytest.fixture
def redis() -> RedisMock:
    return RedisMock()


@pytest.fixture
def redis_strategy(redis):
    return RedisStrategy(redis, 3600)


@pytest.mark.authentication
class TestReadToken:
    @pytest.mark.asyncio
    async def test_missing_token(
        self, redis_strategy: RedisStrategy[UserModel, IDType], user_manager
    ):
        authenticated_user = await redis_strategy.read_token(None, user_manager)
        assert authenticated_user is None

    @pytest.mark.asyncio
    async def test_invalid_token(
        self, redis_strategy: RedisStrategy[UserModel, IDType], user_manager
    ):
        authenticated_user = await redis_strategy.read_token("TOKEN", user_manager)
        assert authenticated_user is None

    @pytest.mark.asyncio
    async def test_valid_token_invalid_uuid(
        self,
        redis_strategy: RedisStrategy[UserModel, IDType],
        redis: RedisMock,
        user_manager,
    ):
        await redis.set(f"{redis_strategy.key_prefix}TOKEN", "bar")
        authenticated_user = await redis_strategy.read_token("TOKEN", user_manager)
        assert authenticated_user is None

    @pytest.mark.asyncio
    async def test_valid_token_not_existing_user(
        self,
        redis_strategy: RedisStrategy[UserModel, IDType],
        redis: RedisMock,
        user_manager,
    ):
        await redis.set(
            f"{redis_strategy.key_prefix}TOKEN", "d35d213e-f3d8-4f08-954a-7e0d1bea286f"
        )
        authenticated_user = await redis_strategy.read_token("TOKEN", user_manager)
        assert authenticated_user is None

    @pytest.mark.asyncio
    async def test_valid_token(
        self,
        redis_strategy: RedisStrategy[UserModel, IDType],
        redis: RedisMock,
        user_manager,
        user,
    ):
        await redis.set(f"{redis_strategy.key_prefix}TOKEN", str(user.id))
        authenticated_user = await redis_strategy.read_token("TOKEN", user_manager)
        assert authenticated_user is not None
        assert authenticated_user.id == user.id


@pytest.mark.authentication
@pytest.mark.asyncio
async def test_write_token(
    redis_strategy: RedisStrategy[UserModel, IDType], redis: RedisMock, user
):
    token = await redis_strategy.write_token(user)

    value = await redis.get(f"{redis_strategy.key_prefix}{token}")
    assert value == str(user.id)


@pytest.mark.authentication
@pytest.mark.asyncio
async def test_destroy_token(
    redis_strategy: RedisStrategy[UserModel, IDType], redis: RedisMock, user
):
    await redis.set(f"{redis_strategy.key_prefix}TOKEN", str(user.id))

    await redis_strategy.destroy_token("TOKEN", user)

    assert await redis.get(f"{redis_strategy.key_prefix}TOKEN") is None
