import asyncio
import json
import os
import signal
import time
from multiprocessing import Process
from unittest import mock

import aiohttp
import pytest
from aiohttp import ClientTimeout
from aiohttp.web import Application
from aiohttp.web_log import AccessLogger
from pytest_toolbox import mktree

from aiohttp_devtools.runserver import run_app, runserver
from aiohttp_devtools.runserver.config import Config
from aiohttp_devtools.runserver.serve import create_auxiliary_app, modify_main_app, src_reload, start_main_app

from .conftest import SIMPLE_APP


async def check_server_running(check_callback):
    port_open = False
    async with aiohttp.ClientSession(timeout=ClientTimeout(total=1)) as session:
        for i in range(50):
            try:
                async with session.get('http://localhost:8000/'):
                    pass
            except OSError:
                await asyncio.sleep(0.1)
            else:
                port_open = True
                break
        assert port_open
        await check_callback(session)


@pytest.mark.boxed
def test_start_runserver(tmpworkdir, smart_caplog):
    mktree(tmpworkdir, {
        'app.py': """\
from aiohttp import web

async def hello(request):
    return web.Response(text='<h1>hello world</h1>', content_type='text/html')

async def has_error(request):
    raise ValueError()

def create_app(loop):
    app = web.Application()
    app.router.add_get('/', hello)
    app.router.add_get('/error', has_error)
    return app""",
        'static_dir/foo.js': 'var bar=1;',
    })
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    aux_app, aux_port, _, _ = runserver(app_path='app.py', static_path='static_dir')
    assert isinstance(aux_app, aiohttp.web.Application)
    assert aux_port == 8001
    for startup in aux_app.on_startup:
        loop.run_until_complete(startup(aux_app))

    async def check_callback(session):
        async with session.get('http://localhost:8000/') as r:
            assert r.status == 200
            assert r.headers['content-type'].startswith('text/html')
            text = await r.text()
            assert '<h1>hello world</h1>' in text
            assert '<script src="http://localhost:8001/livereload.js"></script>' in text

        async with session.get('http://localhost:8000/error') as r:
            assert r.status == 500
            assert 'raise ValueError()' in (await r.text())

    try:
        loop.run_until_complete(check_server_running(check_callback))
    finally:
        for shutdown in aux_app.on_shutdown:
            loop.run_until_complete(shutdown(aux_app))
    assert (
        'adev.server.dft INFO: Starting aux server at http://localhost:8001 ◆\n'
        'adev.server.dft INFO: serving static files from ./static_dir/ at http://localhost:8001/static/\n'
        'adev.server.dft INFO: Starting dev server at http://localhost:8000 ●\n'
    ) in smart_caplog


@pytest.mark.boxed
def test_start_runserver_app_instance(tmpworkdir, loop):
    mktree(tmpworkdir, {
        'app.py': """\
from aiohttp import web

async def hello(request):
    return web.Response(text='<h1>hello world</h1>', content_type='text/html')

app = web.Application()
app.router.add_get('/', hello)
"""
    })
    aux_app, aux_port, _, _ = runserver(app_path='app.py', host='foobar.com')
    assert isinstance(aux_app, aiohttp.web.Application)
    assert aux_port == 8001
    assert len(aux_app.on_startup) == 2
    assert len(aux_app.on_shutdown) == 2


def kill_parent_soon(pid):
    time.sleep(0.2)
    os.kill(pid, signal.SIGINT)


@pytest.mark.boxed
def test_run_app(loop, aiohttp_unused_port):
    app = Application()
    port = aiohttp_unused_port()
    Process(target=kill_parent_soon, args=(os.getpid(),)).start()
    run_app(app, port, loop, AccessLogger)


@pytest.mark.boxed
async def test_run_app_aiohttp_client(tmpworkdir, aiohttp_client):
    mktree(tmpworkdir, SIMPLE_APP)
    config = Config(app_path='app.py')
    app_factory = config.import_app_factory()
    app = app_factory()
    modify_main_app(app, config)
    assert isinstance(app, aiohttp.web.Application)
    cli = await aiohttp_client(app)
    r = await cli.get('/')
    assert r.status == 200
    text = await r.text()
    assert text == 'hello world'


async def test_aux_app(tmpworkdir, aiohttp_client):
    mktree(tmpworkdir, {
        'test.txt': 'test value',
    })
    app = create_auxiliary_app(static_path='.')
    cli = await aiohttp_client(app)
    r = await cli.get('/test.txt')
    assert r.status == 200
    text = await r.text()
    assert text == 'test value'


@pytest.mark.boxed
async def test_serve_main_app(tmpworkdir, loop, mocker):
    asyncio.set_event_loop(loop)
    mktree(tmpworkdir, SIMPLE_APP)
    mock_modify_main_app = mocker.patch('aiohttp_devtools.runserver.serve.modify_main_app')
    loop.call_later(0.5, loop.stop)

    config = Config(app_path='app.py')
    await start_main_app(config, config.import_app_factory(), loop)

    mock_modify_main_app.assert_called_with(mock.ANY, config)


@pytest.mark.boxed
async def test_start_main_app_app_instance(tmpworkdir, loop, mocker):
    mktree(tmpworkdir, {
        'app.py': """\
from aiohttp import web

async def hello(request):
    return web.Response(text='<h1>hello world</h1>', content_type='text/html')

app = web.Application()
app.router.add_get('/', hello)
"""
    })
    mock_modify_main_app = mocker.patch('aiohttp_devtools.runserver.serve.modify_main_app')

    config = Config(app_path='app.py')
    await start_main_app(config, config.import_app_factory(), loop)

    mock_modify_main_app.assert_called_with(mock.ANY, config)


@pytest.yield_fixture
def aux_cli(aiohttp_client, loop):
    app = create_auxiliary_app(static_path='.')
    cli = loop.run_until_complete(aiohttp_client(app))
    yield cli
    loop.run_until_complete(cli.close())


async def test_websocket_hello(aux_cli, smart_caplog):
    async with aux_cli.session.ws_connect(aux_cli.make_url('/livereload')) as ws:
        await ws.send_json({'command': 'hello', 'protocols': ['http://livereload.com/protocols/official-7']})
        async for msg in ws:
            assert msg.type == aiohttp.WSMsgType.text
            data = json.loads(msg.data)
            assert data == {
                'serverName': 'livereload-aiohttp',
                'command': 'hello',
                'protocols': ['http://livereload.com/protocols/official-7']
            }
            break  # noqa
    assert 'adev.server.aux WARNING: browser disconnected, appears no websocket connection was made' in smart_caplog


async def test_websocket_info(aux_cli, loop):
    assert len(aux_cli.server.app['websockets']) == 0
    ws = await aux_cli.session.ws_connect(aux_cli.make_url('/livereload'))
    try:
        await ws.send_json({'command': 'info', 'url': 'foobar', 'plugins': 'bang'})
        await asyncio.sleep(0.05)
        assert len(aux_cli.server.app['websockets']) == 1
    finally:
        await ws.close()


async def test_websocket_bad(aux_cli, smart_caplog):
    async with aux_cli.session.ws_connect(aux_cli.make_url('/livereload')) as ws:
        await ws.send_str('not json')
    async with aux_cli.session.ws_connect(aux_cli.make_url('/livereload')) as ws:
        await ws.send_json({'command': 'hello', 'protocols': ['not official-7']})
    async with aux_cli.session.ws_connect(aux_cli.make_url('/livereload')) as ws:
        await ws.send_json({'command': 'boom', 'url': 'foobar', 'plugins': 'bang'})
    async with aux_cli.session.ws_connect(aux_cli.make_url('/livereload')) as ws:
        await ws.send_bytes(b'this is bytes')
    assert 'adev.server.aux ERROR: live reload protocol 7 not supported' in smart_caplog.log
    assert 'adev.server.aux ERROR: JSON decode error' in smart_caplog.log
    assert 'adev.server.aux ERROR: Unknown ws message' in smart_caplog.log
    assert "adev.server.aux ERROR: unknown websocket message type binary, data: b'this is bytes'" in smart_caplog


async def test_websocket_reload(aux_cli, loop):
    reloads = await src_reload(aux_cli.server.app, 'foobar')
    assert reloads == 0
    ws = await aux_cli.session.ws_connect(aux_cli.make_url('/livereload'))
    try:
        await ws.send_json({
            'command': 'info',
            'url': 'foobar',
            'plugins': 'bang',
        })
        await asyncio.sleep(0.05)
        reloads = await src_reload(aux_cli.server.app, 'foobar')
        assert reloads == 1
    finally:
        await ws.close()