"""
The keepalive job is tested separately on its own using mocking external parts, because the timing-sensitive
behaviours would not be possible to cover otherwise.
"""
import asyncio
import threading
import time
from contextlib import asynccontextmanager, contextmanager

import pytest
from clarabot_nano_common.gateway.job.keepalive import create_resume_observer_thread, state_keepalive_job

from clarabot_nano.gateway.state.gateway import GatewayState


@contextmanager
def time_approx(target: float, epsilon: float):
    t = -time.perf_counter()
    yield
    t += time.perf_counter()
    x = target - t
    assert abs(x) < epsilon, f"{target} {x} {epsilon}"


class _TestingGatewayStateMock(GatewayState):
    def __init__(self):
        # NOTE: Purposefully not calling super __init__!
        self.f = None
        self.e = asyncio.Event()

    @asynccontextmanager
    async def server_request(self, req):  # mocks GatewayState.server_request
        assert req == {"type": "keep-alive"}
        assert self.f is None
        self.f = asyncio.get_running_loop().create_future()
        self.e.set()
        try:
            yield self.f
        finally:
            self.e.clear()
            self.f = None

    @asynccontextmanager
    async def wait_e_check_unset(self, wait_for_e: float, epsilon: float = 0.2, nice: bool = True):
        assert not self.e.is_set()
        with time_approx(wait_for_e, epsilon):
            await self.e.wait()
        yield
        if nice:
            assert self.e.is_set()
            await asyncio.sleep(0.05)
            assert not self.e.is_set()


@pytest.mark.asyncio
async def test_gateway_keepalive_job():
    gws = _TestingGatewayStateMock()
    resume_event = asyncio.Event()
    kaj = asyncio.create_task(
        state_keepalive_job(gws, resume_event, keepalive_wait=3.0, normal_timeout=2.0, eager_timeout=1.0)
    )
    try:
        # first cycle: wait then instant keep-alive
        async with gws.wait_e_check_unset(3.0):
            gws.f.set_result([])

        # second cycle: trigger resume_event then instant keep-alive
        resume_event.set()
        async with gws.wait_e_check_unset(0.0):
            assert resume_event.is_set()
            gws.f.set_result([])

        # third cycle: wait then trigger resume_event then soon keep-alive
        async with gws.wait_e_check_unset(3.0):
            assert not resume_event.is_set()
            resume_event.set()
            await asyncio.sleep(0.05)
            gws.f.set_result([])

        # forth cycle: wait then trigger resume_event and keep-alive simultaneously then soon keep-alive
        async with gws.wait_e_check_unset(3.0):
            assert not resume_event.is_set()
            resume_event.set()
            f1 = gws.f
            gws.f.set_result([])
            await asyncio.sleep(0.05)
            assert gws.f and f1 is not gws.f
            assert resume_event.is_set()
            gws.f.set_result([])

        # fifth cycle: trigger resume_event then wait keep-alive timeout (eager)
        resume_event.set()
        async with gws.wait_e_check_unset(0.0, nice=False):
            assert resume_event.is_set()
            with time_approx(1.0, 0.2):
                await kaj
    finally:
        kaj.cancel()
        await asyncio.gather(kaj, return_exceptions=True)

    resume_event.clear()
    kaj = asyncio.create_task(
        state_keepalive_job(gws, resume_event, keepalive_wait=3.0, normal_timeout=2.0, eager_timeout=1.0)
    )
    try:
        # extra cycle: wait then wait keep-alive timeout (normal)
        async with gws.wait_e_check_unset(3.0, nice=False):
            assert not resume_event.is_set()
            with time_approx(2.0, 0.2):
                await kaj
    finally:
        kaj.cancel()
        await asyncio.gather(kaj, return_exceptions=True)


@pytest.mark.asyncio
async def test_gateway_keepalive_wait_for_resume(monkeypatch):
    time_time = time.time
    delta = 0

    def _time():
        return time_time() + delta

    evt = threading.Event()
    with monkeypatch.context() as m:
        m.setattr(time, "time", _time)

        obs_cb = create_resume_observer_thread(evt.set, poll_interval=0.2)
        try:
            await asyncio.sleep(0.05)
            assert obs_cb() is True
            assert evt.is_set() is False

            await asyncio.sleep(1)
            assert obs_cb() is True
            assert evt.is_set() is False

            delta += 8.5
            await asyncio.sleep(0.3)
            assert obs_cb() is True
            assert evt.is_set() is True
            evt.clear()

            await asyncio.sleep(0.5)
            assert obs_cb() is True
            assert evt.is_set() is False
        finally:
            obs_cb(exit_=True)
