import asyncio

import pytest

from gator.aiojob import (HandlerNotFound, Job, JobSimpleDispatchMixin,
                          NotAJobClassError)


@pytest.fixture
def dispatch_cls():
    class Dispatcher(JobSimpleDispatchMixin):
        pass
    return Dispatcher


def test_simple_coro_registration(dispatch_cls):

    async def handler(job):
        pass

    async def wrong_handler(no_job):
        pass

    with pytest.raises(NotAJobClassError):
        dispatch_cls.register_handler(None, handler)

    with pytest.raises(TypeError):
        dispatch_cls.register_handler(Job, None)

    with pytest.raises(TypeError):
        dispatch_cls.register_handler(Job, wrong_handler)

    dispatch_cls.register_handler(Job, handler)
    disp = dispatch_cls()

    assert dispatch_cls.handlers is disp.handlers
    assert Job in disp.handlers
    assert disp.handlers[Job] == handler


def test_decorator_coro_registration(dispatch_cls):
    with pytest.raises(NotAJobClassError):
        @dispatch_cls.handler(None)
        async def none_handler(job):
            pass

    @dispatch_cls.handler(Job)
    async def handler(job):
        pass

    disp = dispatch_cls()
    assert disp.handlers
    assert Job in disp.handlers
    assert disp.handlers[Job] == handler


@pytest.mark.asyncio
async def test_no_handler(dispatch_cls):
    disp = dispatch_cls()
    job = Job()
    await disp._handle(job)

    assert job.done()
    assert job.state() == 'ERROR'
    assert isinstance(job.exception(), HandlerNotFound)


@pytest.mark.asyncio
async def test_handle_await_call(dispatch_cls):
    @dispatch_cls.handler(Job)
    async def handler(job):
        return 'it works'

    disp = dispatch_cls()
    job = Job()
    res = await disp._handle(job)

    assert res is job
    assert job.done()
    assert job.state() == 'DONE'
    assert job.result() == 'it works'


@pytest.mark.asyncio
async def test_handle_await_job(dispatch_cls):
    @dispatch_cls.handler(Job)
    async def handler(job):
        return 'it works'

    disp = dispatch_cls()
    job = Job()
    asyncio.ensure_future(disp._handle(job))
    await job

    assert job.done()
    assert job.state() == 'DONE'
    assert job.result() == 'it works'


@pytest.mark.asyncio
async def test_flat_function_handler(dispatch_cls):
    @dispatch_cls.handler(Job)
    def handler(job):
        return 'it works'

    disp = dispatch_cls()
    job = Job()
    asyncio.ensure_future(disp._handle(job))
    await job

    assert job.done()
    assert job.state() == 'DONE'
    assert job.result() == 'it works'


@pytest.mark.asyncio
async def test_handle_exception_raised(dispatch_cls):
    class DummyError(Exception):
        pass

    @dispatch_cls.handler(Job)
    async def handler(job):
        raise DummyError()

    disp = dispatch_cls()
    job = await disp._handle(Job())

    assert job.done()
    assert job.state() == 'ERROR'
    assert isinstance(job.exception(), DummyError)


@pytest.mark.asyncio
async def test_handle_timeout(dispatch_cls):

    @dispatch_cls.handler(Job)
    async def handler(job):
        await asyncio.sleep(60)

    disp = dispatch_cls()
    disp.timeout = 0.1
    job = await disp._handle(Job())
    assert job.done()
    assert job.state() == 'ERROR'
    assert isinstance(job.exception(), asyncio.TimeoutError)


@pytest.mark.asyncio
async def test_handler_inheritance(dispatch_cls):
    class Parent(dispatch_cls):
        pass

    class Child(Parent):
        pass

    class JobA(Job):
        pass

    class JobB(Job):
        pass

    class JobC(Job):
        pass

    @Parent.handler(JobA)
    async def handler_a(job):
        return 'handler_a'

    @Parent.handler(JobB)
    async def handler_b(job):
        return 'handler_b'

    @Child.handler(JobB)
    async def handler_b_overload(job):
        return 'handler_b_overload'

    @Child.handler(JobC)
    async def handler_c(job):
        return 'handler_c'

    child = Child()
    parent = Parent()
    assert child.handlers.maps == [
        {
            JobB: handler_b_overload,
            JobC: handler_c,
        },
        {
            JobA: handler_a,
            JobB: handler_b
        },
        {},  # dispatch_cls
        {}   # SimpleDispatchMixin
    ]

    assert parent.handlers.maps == [
        {
            JobA: handler_a,
            JobB: handler_b
        },
        {},  # dispatch_cls
        {}   # SimpleDispatchMixin
    ]

    assert parent.handlers[JobA] == handler_a
    assert parent.handlers[JobB] == handler_b
    assert JobC not in parent.handlers

    assert child.handlers[JobA] == handler_a  # inherited handler
    assert child.handlers[JobB] == handler_b_overload  # overloaded handler
    assert child.handlers[JobC] == handler_c  # specific handler

    job = await parent._handle(JobA())
    assert job.result() == 'handler_a'
    job = await parent._handle(JobB())
    assert job.result() == 'handler_b'
    job = await parent._handle(JobC())
    assert job.state() == 'ERROR'
    assert isinstance(job.exception(), HandlerNotFound)

    job = await child._handle(JobA())
    assert job.result() == 'handler_a'
    job = await child._handle(JobB())
    assert job.result() == 'handler_b_overload'
    job = await child._handle(JobC())
    assert job.result() == 'handler_c'


@pytest.mark.asyncio
async def test_overwrite_handler(dispatch_cls):
    disp = dispatch_cls()

    @disp.handler(Job)
    async def handler_old(job):
        return 'handler_old'

    assert disp.handlers[Job] == handler_old
    job = await disp._handle(Job())
    assert job.result() == 'handler_old'

    @disp.handler(Job)
    async def handler_new(job):
        return 'handler_new'

    assert disp.handlers[Job] == handler_new
    job = await disp._handle(Job())
    assert job.result() == 'handler_new'

    del disp.handlers[Job]
    assert Job not in disp.handlers


@pytest.mark.asyncio
async def test_key_inheritance(dispatch_cls):
    disp = dispatch_cls()

    class Child(Job):
        pass

    @disp.handler(Job)
    async def handler(job):
        return 'handler'

    assert Child not in disp.handlers
    job = await disp._handle(Child())
    assert job.state() == 'DONE'
    assert job.result() == 'handler'
