import asyncio
import json
from unittest.mock import patch

import aiohttp
import async_timeout
import pytest

import peony
import peony.stream
from peony import exceptions
from peony.stream import (DISCONNECTION, DISCONNECTION_TIMEOUT,
                          ENHANCE_YOUR_CALM, ENHANCE_YOUR_CALM_TIMEOUT, EOF,
                          ERROR, ERROR_TIMEOUT, MAX_DISCONNECTION_TIMEOUT,
                          MAX_RECONNECTION_TIMEOUT, NORMAL, RECONNECTION,
                          RECONNECTION_TIMEOUT)

from . import MockResponse

content = [{'text': MockResponse.message + " #%d" % i} for i in range(10)]
data = '\n'.join(json.dumps(line) for line in content) + '\n'


async def stream_content(*args, **kwargs):
    return MockResponse(data=data, status=200)


class Stream:

    async def __aenter__(self):
        self.session = aiohttp.ClientSession()
        self.client = peony.client.BasePeonyClient("", "",
                                                   session=self.session)

        self.patch = patch.object(self.session, 'request',
                                  side_effect=stream_content)
        self.patch.__enter__()

        return peony.stream.StreamResponse(
            client=self.client,
            method='get',
            url="http://whatever.com/stream"
        )

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        self.patch.__exit__()
        self.session.close()
        await self.client.close()


@pytest.mark.asyncio
async def test_stream_connect():
    async with Stream() as stream:
        response = await stream._connect()
        assert data == await response.text()


@pytest.mark.asyncio
async def test_stream_connect_with_session():
    async with aiohttp.ClientSession() as session:
        client = peony.client.BasePeonyClient("", "")

        stream = peony.stream.StreamResponse(
            client=client,
            method='get',
            url="http://whatever.com/stream",
            session=session
        )

        with patch.object(session, 'request', side_effect=stream_content):
            response = await stream._connect()
            assert data == await response.text()


async def _stream_iteration(stream):
    async def stop(*args, **kwargs):
        raise StopAsyncIteration

    with patch.object(stream, 'init_restart', side_effect=stop):
        i = 0
        connected = False
        async for line in stream:
            if connected is False:
                connected = True
                assert 'connected' in line
            else:
                assert line['text'] == MockResponse.message + " #%d" % i
                i += 1


@pytest.mark.asyncio
async def test_stream_iteration():
    async with Stream() as stream:
        await _stream_iteration(stream)


async def response_disconnection():
    return MockResponse(status=500)


async def response_calm():
    return MockResponse(status=429)


async def response_reconnection():
    return MockResponse(status=501)


async def response_forbidden():
    return MockResponse(status=403, content_type='text/plain')


async def response_stream_limit():
    return MockResponse(data="Exceeded connection limit for user", status=200)


async def response_eof(*args, **kwargs):
    return MockResponse(data=data, status=200, eof=True)


@pytest.mark.asyncio
async def test_stream_reconnection_disconnection():
    async def dummy(*args, **kwargs):
        pass

    turn = -1

    async with Stream() as stream:
        with patch.object(stream, '_connect',
                          side_effect=response_disconnection):
            with patch.object(peony.stream.asyncio, 'sleep',
                              side_effect=dummy):
                async for data in stream:
                    assert stream._state == DISCONNECTION
                    turn += 1

                    if turn == 0:
                        assert data == {'connected': True}
                    elif turn % 2 == 1:
                        timeout = DISCONNECTION_TIMEOUT * (turn + 1) / 2

                        if timeout > MAX_DISCONNECTION_TIMEOUT:
                            actual = data['reconnecting_in']
                            assert actual == MAX_DISCONNECTION_TIMEOUT
                            break

                        assert data == {'reconnecting_in': timeout,
                                        'error': None}
                    else:
                        assert data == {'stream_restart': True}


@pytest.mark.asyncio
async def test_stream_reconnection_reconnect():
    async def dummy(*args, **kwargs):
        pass

    turn = -1

    async with Stream() as stream:
        with patch.object(stream, '_connect',
                          side_effect=response_reconnection):
            with patch.object(peony.stream.asyncio, 'sleep',
                              side_effect=dummy):
                async for data in stream:
                    assert stream._state == RECONNECTION
                    turn += 1

                    if turn == 0:
                        assert data == {'connected': True}
                    elif turn % 2 == 1:
                        timeout = RECONNECTION_TIMEOUT * 2**(turn // 2)

                        if timeout > MAX_RECONNECTION_TIMEOUT:
                            actual = data['reconnecting_in']
                            assert actual == MAX_RECONNECTION_TIMEOUT
                            break

                        assert data == {'reconnecting_in': timeout,
                                        'error': None}
                    else:
                        assert data == {'stream_restart': True}


@pytest.mark.asyncio
async def test_stream_eof_reconnect():
    async def dummy(*args, **kwargs):
        pass

    turn = -1

    async with Stream() as stream:
        with patch.object(stream, '_connect',
                          side_effect=response_eof):
            with patch.object(peony.stream.asyncio, 'sleep',
                              side_effect=dummy):
                async for data in stream:
                    turn += 1

                    if turn == 0:
                        assert data == {'connected': True}
                    elif turn % 2 == 1:
                        assert stream._state == EOF
                        assert data == {'reconnecting_in': 0,
                                        'error': None}
                    else:
                        assert data == {'stream_restart': True}
                        break


@pytest.mark.asyncio
async def test_stream_reconnection_enhance_your_calm():
    async def dummy(*args, **kwargs):
        pass

    turn = -1

    async with Stream() as stream:
        with patch.object(stream, '_connect', side_effect=response_calm):
            with patch.object(peony.stream.asyncio, 'sleep',
                              side_effect=dummy):
                async for data in stream:
                    assert stream._state == ENHANCE_YOUR_CALM
                    turn += 1

                    if turn >= 100:
                        break

                    if turn == 0:
                        assert data == {'connected': True}
                    elif turn % 2 == 1:
                        timeout = ENHANCE_YOUR_CALM_TIMEOUT * 2**(turn // 2)
                        assert data == {'reconnecting_in': timeout,
                                        'error': None}
                    else:
                        assert data == {'stream_restart': True}


@pytest.mark.asyncio
async def test_stream_reconnection_error():
    async with Stream() as stream:
        with patch.object(stream, '_connect', side_effect=response_forbidden):
            with pytest.raises(exceptions.Forbidden):
                await stream.connect()


@pytest.mark.asyncio
async def test_stream_reconnection_stream_limit():
    async with Stream() as stream:
        with patch.object(stream, '_connect',
                          side_effect=response_stream_limit):
            assert stream._state == NORMAL
            data = await stream.__anext__()
            assert 'connected' in data

            data = await stream.__anext__()
            assert stream.state == ERROR
            assert data['reconnecting_in'] == ERROR_TIMEOUT
            assert isinstance(data['error'], exceptions.StreamLimit)


@pytest.mark.asyncio
async def test_stream_reconnection_error_on_reconnection():
    async with Stream() as stream:
        with patch.object(stream, '_connect',
                          side_effect=response_disconnection):
            await stream.connect()
            assert stream._state == DISCONNECTION
            data = {'reconnecting_in': DISCONNECTION_TIMEOUT,
                    'error': None}
            assert data == await stream.__anext__()
            assert stream._reconnecting

        with patch.object(stream, '_connect', side_effect=response_calm):
            stream._error_timeout = 0
            assert {'stream_restart': True} == await stream.__anext__()
            assert stream._state == ENHANCE_YOUR_CALM

            data = {'reconnecting_in': ENHANCE_YOUR_CALM_TIMEOUT,
                    'error': None}
            assert data == await stream.__anext__()


@pytest.mark.asyncio
async def test_stream_init_restart_wrong_state():
    async with Stream() as stream:
        stream.state = peony.stream.NORMAL
        with pytest.raises(RuntimeError):
            await stream.init_restart()


@pytest.mark.asyncio
async def test_stream_reconnection_handled_errors():
    async with Stream() as stream:
        async def handled_error():
            raise peony.stream.HandledErrors[0]

        with patch.object(stream, '_connect', side_effect=stream_content):
            data = await stream.__anext__()
            assert 'connected' in data
            with patch.object(stream.response, 'readline',
                              side_effect=handled_error):
                data = await stream.__anext__()
                assert data == {'reconnecting_in': ERROR_TIMEOUT,
                                'error': None}


@pytest.mark.asyncio
async def test_stream_reconnection_client_connection_error():
    async with Stream() as stream:
        async def client_connection_error():
            raise aiohttp.ClientConnectionError

        with patch.object(stream, '_connect', side_effect=stream_content):
            data = await stream.__anext__()
            assert 'connected' in data
            with patch.object(stream.response, 'readline',
                              side_effect=client_connection_error):
                data = await stream.__anext__()
                assert data == {'reconnecting_in': ERROR_TIMEOUT,
                                'error': None}


@pytest.mark.asyncio
async def test_stream_async_context():
    async with aiohttp.ClientSession() as session:
        client = peony.client.BasePeonyClient("", "", session=session)
        context = peony.stream.StreamResponse(method='GET',
                                              url="http://whatever.com/stream",
                                              client=client)

        async with context as stream:
            with patch.object(stream, '_connect', side_effect=stream_content):
                await _stream_iteration(stream)

        assert context.response.closed


@pytest.mark.asyncio
async def test_stream_context():
    async with aiohttp.ClientSession() as session:
        client = peony.client.BasePeonyClient("", "", session=session)
        context = peony.stream.StreamResponse(method='GET',
                                              url="http://whatever.com/stream",
                                              client=client)

        with context as stream:
            with patch.object(stream, '_connect', side_effect=stream_content):
                await _stream_iteration(stream)

        assert context.response.closed


@pytest.mark.asyncio
async def test_stream_context_response_already_closed():
    async with aiohttp.ClientSession() as session:
        client = peony.client.BasePeonyClient("", "", session=session)
        context = peony.stream.StreamResponse(method='GET',
                                              url="http://whatever.com/stream",
                                              client=client)

        with context as stream:
            with patch.object(stream, '_connect', side_effect=stream_content):
                await _stream_iteration(stream)
                stream.response.close()

        assert context.response.closed


@pytest.mark.asyncio
async def test_stream_cancel(event_loop):
    async def cancel(task):
        await asyncio.sleep(0.001)
        task.cancel()

    async def test_stream_iterations(stream):
        async with async_timeout.timeout(0.5):
            while True:
                await _stream_iteration(stream)

    async with aiohttp.ClientSession() as session:
        client = peony.client.BasePeonyClient("", "", session=session)
        context = peony.stream.StreamResponse(method='GET',
                                              url="http://whatever.com",
                                              client=client)

        with context as stream:
            with patch.object(stream, '_connect',
                              side_effect=stream_content):
                coro = test_stream_iterations(stream)
                task = event_loop.create_task(coro)
                cancel_task = event_loop.create_task(cancel(task))

                with async_timeout.timeout(1):
                    await asyncio.wait([task, cancel_task])


@pytest.mark.asyncio
async def test_stream_context_no_response():
    async with aiohttp.ClientSession() as session:
        client = peony.client.BasePeonyClient("", "", session=session)
        stream = peony.stream.StreamResponse(method='GET',
                                             url="http://whatever.com/stream",
                                             client=client)

        assert stream.response is None
        await stream.__aexit__()