from __future__ import absolute_import, unicode_literals

import os

import mock

import tornado.testing
import tornado.wsgi

import mopidy
from mopidy.http import actor, handlers


class HttpServerTest(tornado.testing.AsyncHTTPTestCase):

    def get_config(self):
        return {
            'http': {
                'hostname': '127.0.0.1',
                'port': 6680,
                'static_dir': None,
                'zeroconf': '',
                'allowed_origins': [],
                'csrf_protection': True,
            }
        }

    def get_app(self):
        core = mock.Mock()
        core.get_version = mock.MagicMock(name='get_version')
        core.get_version.return_value = mopidy.__version__

        testapps = [dict(name='testapp')]
        teststatics = [dict(name='teststatic')]

        apps = [{
            'name': 'mopidy',
            'factory': handlers.make_mopidy_app_factory(testapps, teststatics),
        }]

        http_server = actor.HttpServer(
            config=self.get_config(), core=core, sockets=[],
            apps=apps, statics=[])

        return tornado.web.Application(http_server._get_request_handlers())


class RootRedirectTest(HttpServerTest):

    def test_should_redirect_to_mopidy_app(self):
        response = self.fetch('/', method='GET', follow_redirects=False)

        self.assertEqual(response.code, 302)
        self.assertEqual(response.headers['Location'], '/mopidy/')


class LegacyStaticDirAppTest(HttpServerTest):

    def get_config(self):
        config = super(LegacyStaticDirAppTest, self).get_config()
        config['http']['static_dir'] = os.path.dirname(__file__)
        return config

    def test_should_return_index(self):
        response = self.fetch('/', method='GET', follow_redirects=False)

        self.assertEqual(response.code, 404, 'No index.html in this dir')

    def test_should_return_static_files(self):
        response = self.fetch('/test_server.py', method='GET')

        self.assertIn(
            'test_should_return_static_files',
            tornado.escape.to_unicode(response.body))
        self.assertEqual(
            response.headers['X-Mopidy-Version'], mopidy.__version__)
        self.assertEqual(response.headers['Cache-Control'], 'no-cache')


class MopidyAppTest(HttpServerTest):

    def test_should_return_index(self):
        response = self.fetch('/mopidy/', method='GET')
        body = tornado.escape.to_unicode(response.body)

        self.assertIn(
            'This web server is a part of the Mopidy music server.', body)
        self.assertIn('testapp', body)
        self.assertIn('teststatic', body)
        self.assertEqual(
            response.headers['X-Mopidy-Version'], mopidy.__version__)
        self.assertEqual(response.headers['Cache-Control'], 'no-cache')

    def test_without_slash_should_redirect(self):
        response = self.fetch('/mopidy', method='GET', follow_redirects=False)

        self.assertEqual(response.code, 301)
        self.assertEqual(response.headers['Location'], '/mopidy/')

    def test_should_return_static_files(self):
        response = self.fetch('/mopidy/mopidy.js', method='GET')

        self.assertIn(
            'function Mopidy',
            tornado.escape.to_unicode(response.body))
        self.assertEqual(
            response.headers['X-Mopidy-Version'], mopidy.__version__)
        self.assertEqual(response.headers['Cache-Control'], 'no-cache')


class MopidyWebSocketHandlerTest(HttpServerTest):

    def test_should_return_ws(self):
        response = self.fetch('/mopidy/ws', method='GET')

        self.assertEqual(
            'Can "Upgrade" only to "WebSocket".',
            tornado.escape.to_unicode(response.body))

    def test_should_return_ws_old(self):
        response = self.fetch('/mopidy/ws/', method='GET')

        self.assertEqual(
            'Can "Upgrade" only to "WebSocket".',
            tornado.escape.to_unicode(response.body))


class MopidyRPCHandlerTest(HttpServerTest):

    def test_should_return_rpc_error(self):
        cmd = tornado.escape.json_encode({'action': 'get_version'})

        response = self.fetch('/mopidy/rpc', method='POST', body=cmd, headers={
            'Content-Type': 'application/json'})

        self.assertEqual(
            {'jsonrpc': '2.0', 'id': None, 'error':
                {'message': 'Invalid Request', 'code': -32600,
                 'data': '"jsonrpc" member must be included'}},
            tornado.escape.json_decode(response.body))

    def test_should_return_parse_error(self):
        cmd = '{[[[]}'

        response = self.fetch('/mopidy/rpc', method='POST', body=cmd, headers={
            'Content-Type': 'application/json'})

        self.assertEqual(
            {'jsonrpc': '2.0', 'id': None, 'error':
                {'message': 'Parse error', 'code': -32700}},
            tornado.escape.json_decode(response.body))

    def test_should_return_mopidy_version(self):
        cmd = tornado.escape.json_encode({
            'method': 'core.get_version',
            'params': [],
            'jsonrpc': '2.0',
            'id': 1,
        })

        response = self.fetch('/mopidy/rpc', method='POST', body=cmd, headers={
            'Content-Type': 'application/json'})

        self.assertEqual(
            {'jsonrpc': '2.0', 'id': 1, 'result': mopidy.__version__},
            tornado.escape.json_decode(response.body))

    def test_should_return_extra_headers(self):
        response = self.fetch('/mopidy/rpc', method='HEAD')

        self.assertIn('Accept', response.headers)
        self.assertIn('X-Mopidy-Version', response.headers)
        self.assertIn('Cache-Control', response.headers)
        self.assertIn('Content-Type', response.headers)

    def test_should_require_correct_content_type(self):
        cmd = tornado.escape.json_encode({
            'method': 'core.get_version',
            'params': [],
            'jsonrpc': '2.0',
            'id': 1,
        })

        response = self.fetch('/mopidy/rpc', method='POST', body=cmd, headers={
            'Content-Type': 'text/plain'})

        self.assertEqual(response.code, 415)
        self.assertEqual(
            response.reason, 'Content-Type must be application/json')

    def test_different_origin_returns_access_denied(self):
        response = self.fetch('/mopidy/rpc', method='OPTIONS', headers={
            'Host': 'me:6680', 'Origin': 'http://evil:666'})

        self.assertEqual(response.code, 403)
        self.assertEqual(
            response.reason, 'Access denied for origin http://evil:666')

    def test_same_origin_returns_cors_headers(self):
        response = self.fetch('/mopidy/rpc', method='OPTIONS', headers={
            'Host': 'me:6680', 'Origin': 'http://me:6680'})

        self.assertEqual(
            response.headers['Access-Control-Allow-Origin'], 'http://me:6680')
        self.assertEqual(
            response.headers['Access-Control-Allow-Headers'], 'Content-Type')


class MopidyRPCHandlerNoCSRFProtectionTest(HttpServerTest):

    def get_config(self):
        config = super(MopidyRPCHandlerNoCSRFProtectionTest, self).get_config()
        config['http']['csrf_protection'] = False
        return config

    def get_cmd(self):
        return tornado.escape.json_encode({
            'method': 'core.get_version',
            'params': [],
            'jsonrpc': '2.0',
            'id': 1,
        })

    def test_should_ignore_incorrect_content_type(self):
        response = self.fetch(
            '/mopidy/rpc', method='POST', body=self.get_cmd(),
            headers={'Content-Type': 'text/plain'})

        self.assertEqual(response.code, 200)

    def test_should_ignore_missing_content_type(self):
        response = self.fetch(
            '/mopidy/rpc', method='POST', body=self.get_cmd(), headers={})

        self.assertEqual(response.code, 200)

    def test_different_origin_returns_allowed(self):
        response = self.fetch('/mopidy/rpc', method='OPTIONS', headers={
            'Host': 'me:6680', 'Origin': 'http://evil:666'})

        self.assertEqual(response.code, 204)

    def test_should_not_return_cors_headers(self):
        response = self.fetch('/mopidy/rpc', method='OPTIONS', headers={
            'Host': 'me:6680', 'Origin': 'http://me:6680'})

        self.assertNotIn('Access-Control-Allow-Origin', response.headers)
        self.assertNotIn('Access-Control-Allow-Headers', response.headers)


class HttpServerWithStaticFilesTest(tornado.testing.AsyncHTTPTestCase):

    def get_app(self):
        config = {
            'http': {
                'hostname': '127.0.0.1',
                'port': 6680,
                'static_dir': None,
                'zeroconf': '',
            }
        }
        core = mock.Mock()

        statics = [dict(name='static', path=os.path.dirname(__file__))]

        http_server = actor.HttpServer(
            config=config, core=core, sockets=[], apps=[], statics=statics)

        return tornado.web.Application(http_server._get_request_handlers())

    def test_without_slash_should_redirect(self):
        response = self.fetch('/static', method='GET', follow_redirects=False)

        self.assertEqual(response.code, 301)
        self.assertEqual(response.headers['Location'], '/static/')

    def test_can_serve_static_files(self):
        response = self.fetch('/static/test_server.py', method='GET')

        self.assertEqual(200, response.code)
        self.assertEqual(
            response.headers['X-Mopidy-Version'], mopidy.__version__)
        self.assertEqual(
            response.headers['Cache-Control'], 'no-cache')


def wsgi_app_factory(config, core):

    def wsgi_app(environ, start_response):
        status = '200 OK'
        response_headers = [('Content-type', 'text/plain')]
        start_response(status, response_headers)
        return ['Hello, world!\n']

    return [
        ('(.*)', tornado.web.FallbackHandler, {
            'fallback': tornado.wsgi.WSGIContainer(wsgi_app),
        }),
    ]


class HttpServerWithWsgiAppTest(tornado.testing.AsyncHTTPTestCase):

    def get_app(self):
        config = {
            'http': {
                'hostname': '127.0.0.1',
                'port': 6680,
                'static_dir': None,
                'zeroconf': '',
            }
        }
        core = mock.Mock()

        apps = [{
            'name': 'wsgi',
            'factory': wsgi_app_factory,
        }]

        http_server = actor.HttpServer(
            config=config, core=core, sockets=[], apps=apps, statics=[])

        return tornado.web.Application(http_server._get_request_handlers())

    def test_without_slash_should_redirect(self):
        response = self.fetch('/wsgi', method='GET', follow_redirects=False)

        self.assertEqual(response.code, 301)
        self.assertEqual(response.headers['Location'], '/wsgi/')

    def test_can_wrap_wsgi_apps(self):
        response = self.fetch('/wsgi/', method='GET')

        self.assertEqual(200, response.code)
        self.assertIn(
            'Hello, world!', tornado.escape.to_unicode(response.body))
