import asyncio
import unittest
from unittest import mock
from aiohttp import CIMultiDict
from aiohttp.web import (
    MsgType, Request, WebSocketResponse, HTTPMethodNotAllowed, HTTPBadRequest)
from aiohttp.protocol import RawRequestMessage, HttpVersion11
from aiohttp import errors, signals, websocket


class TestWebWebSocket(unittest.TestCase):

    def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(None)

    def tearDown(self):
        self.loop.close()

    def make_request(self, method, path, headers=None, protocols=False):
        self.app = mock.Mock()
        self.app._debug = False
        if headers is None:
            headers = CIMultiDict(
                {'HOST': 'server.example.com',
                 'UPGRADE': 'websocket',
                 'CONNECTION': 'Upgrade',
                 'SEC-WEBSOCKET-KEY': 'dGhlIHNhbXBsZSBub25jZQ==',
                 'ORIGIN': 'http://example.com',
                 'SEC-WEBSOCKET-VERSION': '13'})
            if protocols:
                headers['SEC-WEBSOCKET-PROTOCOL'] = 'chat, superchat'

        message = RawRequestMessage(method, path, HttpVersion11, headers,
                                    False, False)
        self.payload = mock.Mock()
        self.transport = mock.Mock()
        self.reader = mock.Mock()
        self.writer = mock.Mock()
        self.app.loop = self.loop
        self.app.on_response_prepare = signals.Signal(self.app)
        req = Request(self.app, message, self.payload,
                      self.transport, self.reader, self.writer)
        return req

    def test_nonstarted_ping(self):
        ws = WebSocketResponse()
        with self.assertRaises(RuntimeError):
            ws.ping()

    def test_nonstarted_pong(self):
        ws = WebSocketResponse()
        with self.assertRaises(RuntimeError):
            ws.pong()

    def test_nonstarted_send_str(self):
        ws = WebSocketResponse()
        with self.assertRaises(RuntimeError):
            ws.send_str('string')

    def test_nonstarted_send_bytes(self):
        ws = WebSocketResponse()
        with self.assertRaises(RuntimeError):
            ws.send_bytes(b'bytes')

    def test_nonstarted_close(self):
        ws = WebSocketResponse()
        with self.assertRaises(RuntimeError):
            self.loop.run_until_complete(ws.close())

    def test_nonstarted_receive_str(self):

        @asyncio.coroutine
        def go():
            ws = WebSocketResponse()
            with self.assertRaises(RuntimeError):
                yield from ws.receive_str()

        self.loop.run_until_complete(go())

    def test_nonstarted_receive_bytes(self):

        @asyncio.coroutine
        def go():
            ws = WebSocketResponse()
            with self.assertRaises(RuntimeError):
                yield from ws.receive_bytes()

        self.loop.run_until_complete(go())

    def test_receive_str_nonstring(self):

        @asyncio.coroutine
        def go():
            req = self.make_request('GET', '/')
            ws = WebSocketResponse()
            yield from ws.prepare(req)

            @asyncio.coroutine
            def receive():
                return websocket.Message(websocket.MSG_BINARY, b'data', b'')

            ws.receive = receive

            with self.assertRaises(TypeError):
                yield from ws.receive_str()

        self.loop.run_until_complete(go())

    def test_receive_bytes_nonsbytes(self):

        @asyncio.coroutine
        def go():
            req = self.make_request('GET', '/')
            ws = WebSocketResponse()
            yield from ws.prepare(req)

            @asyncio.coroutine
            def receive():
                return websocket.Message(websocket.MSG_TEXT, 'data', b'')

            ws.receive = receive

            with self.assertRaises(TypeError):
                yield from ws.receive_bytes()

        self.loop.run_until_complete(go())

    def test_send_str_nonstring(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        with self.assertRaises(TypeError):
            ws.send_str(b'bytes')

    def test_send_bytes_nonbytes(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        with self.assertRaises(TypeError):
            ws.send_bytes('string')

    def test_write(self):
        ws = WebSocketResponse()
        with self.assertRaises(RuntimeError):
            ws.write(b'data')

    def test_can_prepare_ok(self):
        req = self.make_request('GET', '/', protocols=True)
        ws = WebSocketResponse(protocols=('chat',))
        self.assertEqual((True, 'chat'), ws.can_prepare(req))

    def test_can_prepare_unknown_protocol(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.assertEqual((True, None), ws.can_prepare(req))

    def test_can_prepare_invalid_method(self):
        req = self.make_request('POST', '/')
        ws = WebSocketResponse()
        self.assertEqual((False, None), ws.can_prepare(req))

    def test_can_prepare_without_upgrade(self):
        req = self.make_request('GET', '/',
                                headers=CIMultiDict({}))
        ws = WebSocketResponse()
        self.assertEqual((False, None), ws.can_prepare(req))

    def test_can_prepare_started(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        with self.assertRaisesRegex(RuntimeError, 'Already started'):
            ws.can_prepare(req)

    def test_closed_after_ctor(self):
        ws = WebSocketResponse()
        self.assertFalse(ws.closed)
        self.assertIsNone(ws.close_code)

    def test_send_str_closed(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        self.loop.run_until_complete(ws.close())
        with self.assertRaises(RuntimeError):
            ws.send_str('string')

    def test_send_bytes_closed(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        self.loop.run_until_complete(ws.close())
        with self.assertRaises(RuntimeError):
            ws.send_bytes(b'bytes')

    def test_ping_closed(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        self.loop.run_until_complete(ws.close())
        with self.assertRaises(RuntimeError):
            ws.ping()

    def test_pong_closed(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        self.loop.run_until_complete(ws.close())
        with self.assertRaises(RuntimeError):
            ws.pong()

    def test_close_idempotent(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        writer = mock.Mock()
        ws._writer = writer
        self.assertTrue(
            self.loop.run_until_complete(ws.close(code=1, message='message1')))
        self.assertTrue(ws.closed)
        self.assertFalse(
            self.loop.run_until_complete(ws.close(code=2, message='message2')))

    def test_start_invalid_method(self):
        req = self.make_request('POST', '/')
        ws = WebSocketResponse()
        with self.assertRaises(HTTPMethodNotAllowed):
            self.loop.run_until_complete(ws.prepare(req))

    def test_start_without_upgrade(self):
        req = self.make_request('GET', '/',
                                headers=CIMultiDict({}))
        ws = WebSocketResponse()
        with self.assertRaises(HTTPBadRequest):
            self.loop.run_until_complete(ws.prepare(req))

    def test_wait_closed_before_start(self):

        @asyncio.coroutine
        def go():
            ws = WebSocketResponse()
            with self.assertRaises(RuntimeError):
                yield from ws.close()

        self.loop.run_until_complete(go())

    def test_write_eof_not_started(self):

        @asyncio.coroutine
        def go():
            ws = WebSocketResponse()
            with self.assertRaises(RuntimeError):
                yield from ws.write_eof()

        self.loop.run_until_complete(go())

    def test_write_eof_idempotent(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        self.loop.run_until_complete(ws.close())

        @asyncio.coroutine
        def go():
            yield from ws.write_eof()
            yield from ws.write_eof()
            yield from ws.write_eof()

        self.loop.run_until_complete(go())

    def test_receive_exc_in_reader(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))

        exc = ValueError()
        res = asyncio.Future(loop=self.loop)
        res.set_exception(exc)
        ws._reader.read.return_value = res

        @asyncio.coroutine
        def go():
            msg = yield from ws.receive()
            self.assertTrue(msg.tp, MsgType.error)
            self.assertIs(msg.data, exc)
            self.assertIs(ws.exception(), exc)

        self.loop.run_until_complete(go())

    def test_receive_cancelled(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))

        res = asyncio.Future(loop=self.loop)
        res.set_exception(asyncio.CancelledError())
        ws._reader.read.return_value = res

        self.assertRaises(
            asyncio.CancelledError,
            self.loop.run_until_complete, ws.receive())

    def test_receive_timeouterror(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))

        res = asyncio.Future(loop=self.loop)
        res.set_exception(asyncio.TimeoutError())
        ws._reader.read.return_value = res

        self.assertRaises(
            asyncio.TimeoutError,
            self.loop.run_until_complete, ws.receive())

    def test_receive_client_disconnected(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))

        exc = errors.ClientDisconnectedError()
        res = asyncio.Future(loop=self.loop)
        res.set_exception(exc)
        ws._reader.read.return_value = res

        @asyncio.coroutine
        def go():
            msg = yield from ws.receive()
            self.assertTrue(ws.closed)
            self.assertTrue(msg.tp, MsgType.close)
            self.assertIs(msg.data, None)
            self.assertIs(ws.exception(), None)

        self.loop.run_until_complete(go())

    def test_multiple_receive_on_close_connection(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        self.loop.run_until_complete(ws.close())
        self.loop.run_until_complete(ws.receive())
        self.loop.run_until_complete(ws.receive())
        self.loop.run_until_complete(ws.receive())
        self.loop.run_until_complete(ws.receive())
        self.assertRaises(
            RuntimeError, self.loop.run_until_complete, ws.receive())

    def test_concurrent_receive(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))
        ws._waiting = True

        self.assertRaises(
            RuntimeError, self.loop.run_until_complete, ws.receive())

    def test_close_exc(self):
        req = self.make_request('GET', '/')
        reader = self.reader.set_parser.return_value = mock.Mock()

        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))

        exc = ValueError()
        reader.read.return_value = asyncio.Future(loop=self.loop)
        reader.read.return_value.set_exception(exc)

        self.loop.run_until_complete(ws.close())
        self.assertTrue(ws.closed)
        self.assertIs(ws.exception(), exc)

        ws._closed = False
        reader.read.return_value = asyncio.Future(loop=self.loop)
        reader.read.return_value.set_exception(asyncio.CancelledError())
        self.assertRaises(asyncio.CancelledError,
                          self.loop.run_until_complete, ws.close())
        self.assertEqual(ws.close_code, 1006)

    def test_close_exc2(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        self.loop.run_until_complete(ws.prepare(req))

        exc = ValueError()
        self.writer.close.side_effect = exc
        ws._writer = self.writer

        self.loop.run_until_complete(ws.close())
        self.assertTrue(ws.closed)
        self.assertIs(ws.exception(), exc)

        ws._closed = False
        self.writer.close.side_effect = asyncio.CancelledError()
        self.assertRaises(asyncio.CancelledError,
                          self.loop.run_until_complete, ws.close())

    def test_start_twice_idempotent(self):
        req = self.make_request('GET', '/')
        ws = WebSocketResponse()
        with self.assertWarns(DeprecationWarning):
            impl1 = ws.start(req)
            impl2 = ws.start(req)
            self.assertIs(impl1, impl2)

    def test_can_start_ok(self):
        req = self.make_request('GET', '/', protocols=True)
        ws = WebSocketResponse(protocols=('chat',))
        with self.assertWarns(DeprecationWarning):
            self.assertEqual((True, 'chat'), ws.can_start(req))
