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

import aiohttp
import pytest

from clarabot_nano.server.general.cachedb import IndexCacheDatabase
from clarabot_nano.server.general.handler.base import IndexCachedHandlerMixin, GeneralServerHandleHelper
from clarabot_nano.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)  # 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, paths: List[Tuple[Path, str]]) -> Tuple[Any, str, Optional[Any]]:
        r = await IndexCachedHandlerMixin.index_cached_value(self, paths)
        assert not self.parent_validity
        assert not self.load
        assert not self.make
        return r

    async def get_parent_validity(self, accessor_path: str) -> 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] != 1:
            return None
        return cached_value

    async def _make_cached(self, paths, accessor_path) -> Tuple[Any, Optional[Any]]:
        await asyncio.sleep(self.make_delay)
        data = self.make.pop(0)
        return [1, data[0]], data


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

            ps0 = [(Path("/tmp/test-r0/x"), "N/A")]
            ps1 = [*ps0, (Path("/tmp/test-r0/x/q"), "N/A")]
            ps2 = [*ps1, (Path("/tmp/test-r0/x/q/z"), "N/A")]

            k0 = h.index_cache_key(ps0)
            k1 = h.index_cache_key(ps1)
            k2 = h.index_cache_key(ps2)
            assert k0 == b"\x92\xa8ICH-TEST\xae/tmp/test-r0/x"
            assert k1 == b"\x92\xa8ICH-TEST\xb0/tmp/test-r0/x/q"
            assert k2 == b"\x92\xa8ICH-TEST\xb2/tmp/test-r0/x/q/z"

            # not cached because validity is different
            assert await db.get(k0) is None
            h.setup_mock([0, 1], 0, [(540, 870)])
            assert await h.index_cached_value(ps0) == ([1, 540], "/tmp/test-r0/x/", (540, 870))
            assert await db.get(k0) is None

            # cached
            assert await db.get(k0) is None
            h.setup_mock([8, 8], 0, [(541, 871)])
            assert await h.index_cached_value(ps0) == ([1, 541], "/tmp/test-r0/x/", (541, 871))
            assert await db.get(k0) == (b"\x92\x01\xcd\x02\x1d", b"\x00\x00\x00\x00\x00\x00\x00\x08")

            # recalled from cache
            assert await db.get(k0) == (b"\x92\x01\xcd\x02\x1d", b"\x00\x00\x00\x00\x00\x00\x00\x08")
            h.setup_mock([8], 1, [])
            assert await h.index_cached_value(ps0) == ([1, 541], "/tmp/test-r0/x/", None)
            assert await db.get(k0) == (b"\x92\x01\xcd\x02\x1d", b"\x00\x00\x00\x00\x00\x00\x00\x08")

            # cached is obsolete, renew
            await db.set(k0, b"xxxx", b"\x00\x00\x00\x00\x00\x00\x00\x08")
            assert await db.get(k0) == (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(ps0) == ([1, 542], "/tmp/test-r0/x/", (542, 872))
            assert await db.get(k0) == (b"\x92\x01\xcd\x02\x1e", b"\x00\x00\x00\x00\x00\x00\x00\x08")

            # recalled from cache
            assert await db.get(k0) == (b"\x92\x01\xcd\x02\x1e", b"\x00\x00\x00\x00\x00\x00\x00\x08")
            h.setup_mock([8], 1, [])
            assert await h.index_cached_value(ps0) == ([1, 542], "/tmp/test-r0/x/", None)
            assert await db.get(k0) == (b"\x92\x01\xcd\x02\x1e", b"\x00\x00\x00\x00\x00\x00\x00\x08")

            # cached is expired, renew
            assert await db.get(k0) == (b"\x92\x01\xcd\x02\x1e", b"\x00\x00\x00\x00\x00\x00\x00\x08")
            h.setup_mock([9, 9], 0, [(543, 873)])
            assert await h.index_cached_value(ps0) == ([1, 543], "/tmp/test-r0/x/", (543, 873))
            assert await db.get(k0) == (b"\x92\x01\xcd\x02\x1f", 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(k0) == (b"\x92\x01\xcd\x02\x1f", b"\x00\x00\x00\x00\x00\x00\x00\x09")
            assert await db.get(k2) is None
            h.setup_mock([10, 10], 0, [(544, 874)])
            t = time.perf_counter()
            ts = [asyncio.create_task(h.index_cached_value(ps2)) for _ in range(2)]
            assert await asyncio.gather(*ts) == [([1, 544], "/tmp/test-r0/x//q/z", (544, 874))] * 2
            assert 0.4 < time.perf_counter() - t < 0.6
            assert await db.get(k0) == (b"\x92\x01\xcd\x02\x1f", b"\x00\x00\x00\x00\x00\x00\x00\x09")
            assert await db.get(k2) == (b"\x92\x01\xcd\x02 ", b"\x00\x00\x00\x00\x00\x00\x00\n")
