import asyncio
import time
from pathlib import Path
from typing import Any, List

import aiohttp
import pytest
from clarabot_nano_common.server.general.cachedb import IndexCacheDatabase
from clarabot_nano_common.server.general.handler.base import (
    IndexCachedHandlerMixin,
    GeneralServerHandleHelper,
    IndexCachedHandlerValueArgs,
)
from clarabot_nano_common.utility.serialize import msgpack_loads, ulong2bytes

from clarabot_nano_tests.testing.file import temp_path


class _TestingIndexCachedHandlerMixin(IndexCachedHandlerMixin):
    def __init__(self, session, db):
        self.HANDLER_KEY = "ICH-TEST"
        self.helper = GeneralServerHandleHelper(None, session, db, None, None)  # others N/A
        # extracted_config is N/A
        # mock states
        self.parent_validity = []
        self.load = []
        self.make = []
        self.make_delay = 0.0

    def setup_mock(self, parent_validity: List[int], load: int, make: List[Any]):
        self.parent_validity = parent_validity
        self.load = [None] * load
        self.make = make

    async def index_cached_value(self, cache_args: IndexCachedHandlerValueArgs) -> asyncio.Future[Any]:
        r = await IndexCachedHandlerMixin.index_cached_value(self, cache_args)
        assert not self.parent_validity
        assert not self.load
        assert not self.make
        return r

    async def _get_parent_validity(self, cache_args: IndexCachedHandlerValueArgs) -> bytes:
        v = self.parent_validity.pop(0)
        return ulong2bytes(v)

    def _load_cached(self, cached_value: bytes) -> Any:
        self.load.pop()
        try:
            cached_value = msgpack_loads(cached_value)
        except Exception:
            return None
        if not isinstance(cached_value, (list, tuple)) or len(cached_value) != 2 or cached_value[0] != "x":
            return None
        return cached_value

    async def _make_cached(self, cache_args: IndexCachedHandlerValueArgs) -> Any:
        await asyncio.sleep(self.make_delay)
        data = self.make.pop(0)
        return ["x", sum(data)]


@pytest.mark.asyncio
async def test_index_cached_handler_mixin(tmpdir):
    async with IndexCacheDatabase.create(str(temp_path(tmpdir)), 10.0) as db:
        async with aiohttp.ClientSession() as session:
            h = _TestingIndexCachedHandlerMixin(session, db)

            ca0 = IndexCachedHandlerValueArgs(h.HANDLER_KEY, [(Path("/tmp/test-r0/x"), "N/A")])
            ca1 = IndexCachedHandlerValueArgs(h.HANDLER_KEY, [*ca0.paths, (Path("/tmp/test-r0/x/q"), "N/A")])
            ca2 = IndexCachedHandlerValueArgs(h.HANDLER_KEY, [*ca1.paths, (Path("/tmp/test-r0/x/q/z"), "N/A")])

            assert ca0.cache_key == b"\x92\xa8ICH-TEST\xae/tmp/test-r0/x"
            assert ca1.cache_key == b"\x92\xa8ICH-TEST\xb0/tmp/test-r0/x/q"
            assert ca2.cache_key == b"\x92\xa8ICH-TEST\xb2/tmp/test-r0/x/q/z"

            # not cached because validity is different
            assert await db.get(ca0.cache_key) is None
            h.setup_mock([0, 1], 0, [(540, 870)])
            assert await h.index_cached_value(ca0) == ["x", 1410]
            assert await db.get(ca0.cache_key) is None

            # cached
            assert await db.get(ca0.cache_key) is None
            h.setup_mock([8, 8], 0, [(541, 871)])
            assert await h.index_cached_value(ca0) == ["x", 1412]
            assert await db.get(ca0.cache_key) == (b"\x92\xa1x\xcd\x05\x84", b"\x00\x00\x00\x00\x00\x00\x00\x08")

            # recalled from cache
            assert await db.get(ca0.cache_key) == (b"\x92\xa1x\xcd\x05\x84", b"\x00\x00\x00\x00\x00\x00\x00\x08")
            h.setup_mock([8], 1, [])
            assert await h.index_cached_value(ca0) == ["x", 1412]
            assert await db.get(ca0.cache_key) == (b"\x92\xa1x\xcd\x05\x84", b"\x00\x00\x00\x00\x00\x00\x00\x08")

            # cached is obsolete, renew
            await db.set(ca0.cache_key, b"xxxx", b"\x00\x00\x00\x00\x00\x00\x00\x08", 5000)
            assert await db.get(ca0.cache_key) == (b"xxxx", b"\x00\x00\x00\x00\x00\x00\x00\x08")
            h.setup_mock([8, 8], 1, [(542, 872)])
            assert await h.index_cached_value(ca0) == ["x", 1414]
            assert await db.get(ca0.cache_key) == (b"\x92\xa1x\xcd\x05\x86", b"\x00\x00\x00\x00\x00\x00\x00\x08")

            # recalled from cache
            assert await db.get(ca0.cache_key) == (b"\x92\xa1x\xcd\x05\x86", b"\x00\x00\x00\x00\x00\x00\x00\x08")
            h.setup_mock([8], 1, [])
            assert await h.index_cached_value(ca0) == ["x", 1414]
            assert await db.get(ca0.cache_key) == (b"\x92\xa1x\xcd\x05\x86", b"\x00\x00\x00\x00\x00\x00\x00\x08")

            # cached is expired, renew
            assert await db.get(ca0.cache_key) == (b"\x92\xa1x\xcd\x05\x86", b"\x00\x00\x00\x00\x00\x00\x00\x08")
            h.setup_mock([9, 9], 0, [(543, 873)])
            assert await h.index_cached_value(ca0) == ["x", 1416]
            assert await db.get(ca0.cache_key) == (b"\x92\xa1x\xcd\x05\x88", b"\x00\x00\x00\x00\x00\x00\x00\x09")

            # another cached, parallel are synced into a single task
            h.make_delay = 0.5
            assert await db.get(ca0.cache_key) == (b"\x92\xa1x\xcd\x05\x88", b"\x00\x00\x00\x00\x00\x00\x00\x09")
            assert await db.get(ca2.cache_key) is None
            h.setup_mock([10, 10], 0, [(544, 874)])
            t = time.perf_counter()
            ts = [asyncio.create_task(h.index_cached_value(ca2)) for _ in range(2)]
            assert await asyncio.gather(*ts) == [["x", 1418]] * 2
            assert 0.4 < time.perf_counter() - t < 0.6
            assert await db.get(ca0.cache_key) == (b"\x92\xa1x\xcd\x05\x88", b"\x00\x00\x00\x00\x00\x00\x00\x09")
            assert await db.get(ca2.cache_key) == (b"\x92\xa1x\xcd\x05\x8a", b"\x00\x00\x00\x00\x00\x00\x00\n")
