from __future__ import unicode_literals

import json
import unittest

import mock

import pykka

from mopidy import core, models
from mopidy.backend import dummy
from mopidy.utils import jsonrpc


class Calculator(object):
    def model(self):
        return 'TI83'

    def add(self, a, b):
        """Returns the sum of the given numbers"""
        return a + b

    def sub(self, a, b):
        return a - b

    def describe(self):
        return {
            'add': 'Returns the sum of the terms',
            'sub': 'Returns the diff of the terms',
        }

    def take_it_all(self, a, b, c=True, *args, **kwargs):
        pass

    def _secret(self):
        return 'Grand Unified Theory'

    def fail(self):
        raise ValueError('What did you expect?')


class JsonRpcTestBase(unittest.TestCase):
    def setUp(self):
        self.backend = dummy.create_dummy_backend_proxy()
        self.core = core.Core.start(backends=[self.backend]).proxy()

        self.jrw = jsonrpc.JsonRpcWrapper(
            objects={
                'hello': lambda: 'Hello, world!',
                'calc': Calculator(),
                'core': self.core,
                'core.playback': self.core.playback,
                'core.tracklist': self.core.tracklist,
                'get_uri_schemes': self.core.get_uri_schemes,
            },
            encoders=[models.ModelJSONEncoder],
            decoders=[models.model_json_decoder])

    def tearDown(self):
        pykka.ActorRegistry.stop_all()


class JsonRpcSetupTest(JsonRpcTestBase):
    def test_empty_object_mounts_is_not_allowed(self):
        test = lambda: jsonrpc.JsonRpcWrapper(objects={'': Calculator()})
        self.assertRaises(AttributeError, test)


class JsonRpcSerializationTest(JsonRpcTestBase):
    def test_handle_json_converts_from_and_to_json(self):
        self.jrw.handle_data = mock.Mock()
        self.jrw.handle_data.return_value = {'foo': 'response'}

        request = '{"foo": "request"}'
        response = self.jrw.handle_json(request)

        self.jrw.handle_data.assert_called_once_with({'foo': 'request'})
        self.assertEqual(response, '{"foo": "response"}')

    def test_handle_json_decodes_mopidy_models(self):
        self.jrw.handle_data = mock.Mock()
        self.jrw.handle_data.return_value = []

        request = '{"foo": {"__model__": "Artist", "name": "bar"}}'
        self.jrw.handle_json(request)

        self.jrw.handle_data.assert_called_once_with(
            {'foo': models.Artist(name='bar')})

    def test_handle_json_encodes_mopidy_models(self):
        self.jrw.handle_data = mock.Mock()
        self.jrw.handle_data.return_value = {'foo': models.Artist(name='bar')}

        request = '[]'
        response = json.loads(self.jrw.handle_json(request))

        self.assertIn('foo', response)
        self.assertIn('__model__', response['foo'])
        self.assertEqual(response['foo']['__model__'], 'Artist')
        self.assertIn('name', response['foo'])
        self.assertEqual(response['foo']['name'], 'bar')

    def test_handle_json_returns_nothing_for_notices(self):
        request = '{"jsonrpc": "2.0", "method": "core.get_uri_schemes"}'
        response = self.jrw.handle_json(request)

        self.assertEqual(response, None)

    def test_invalid_json_command_causes_parse_error(self):
        request = (
            '{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]')
        response = self.jrw.handle_json(request)
        response = json.loads(response)

        self.assertEqual(response['jsonrpc'], '2.0')
        error = response['error']
        self.assertEqual(error['code'], -32700)
        self.assertEqual(error['message'], 'Parse error')

    def test_invalid_json_batch_causes_parse_error(self):
        request = """[
            {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
            {"jsonrpc": "2.0", "method"
        ]"""
        response = self.jrw.handle_json(request)
        response = json.loads(response)

        self.assertEqual(response['jsonrpc'], '2.0')
        error = response['error']
        self.assertEqual(error['code'], -32700)
        self.assertEqual(error['message'], 'Parse error')


class JsonRpcSingleCommandTest(JsonRpcTestBase):
    def test_call_method_on_root(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'hello',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertEqual(response['jsonrpc'], '2.0')
        self.assertEqual(response['id'], 1)
        self.assertNotIn('error', response)
        self.assertEqual(response['result'], 'Hello, world!')

    def test_call_method_on_plain_object(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'calc.model',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertEqual(response['jsonrpc'], '2.0')
        self.assertEqual(response['id'], 1)
        self.assertNotIn('error', response)
        self.assertEqual(response['result'], 'TI83')

    def test_call_method_which_returns_dict_from_plain_object(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'calc.describe',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertEqual(response['jsonrpc'], '2.0')
        self.assertEqual(response['id'], 1)
        self.assertNotIn('error', response)
        self.assertIn('add', response['result'])
        self.assertIn('sub', response['result'])

    def test_call_method_on_actor_root(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'core.get_uri_schemes',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertEqual(response['jsonrpc'], '2.0')
        self.assertEqual(response['id'], 1)
        self.assertNotIn('error', response)
        self.assertEqual(response['result'], ['dummy'])

    def test_call_method_on_actor_member(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'core.playback.get_volume',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertEqual(response['result'], None)

    def test_call_method_which_is_a_directly_mounted_actor_member(self):
        # 'get_uri_schemes' isn't a regular callable, but a Pykka
        # CallableProxy. This test checks that CallableProxy objects are
        # threated by JsonRpcWrapper like any other callable.

        request = {
            'jsonrpc': '2.0',
            'method': 'get_uri_schemes',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertEqual(response['jsonrpc'], '2.0')
        self.assertEqual(response['id'], 1)
        self.assertNotIn('error', response)
        self.assertEqual(response['result'], ['dummy'])

    def test_call_method_with_positional_params(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'core.playback.set_volume',
            'params': [37],
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertEqual(response['result'], None)
        self.assertEqual(self.core.playback.get_volume().get(), 37)

    def test_call_methods_with_named_params(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'core.playback.set_volume',
            'params': {'volume': 37},
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertEqual(response['result'], None)
        self.assertEqual(self.core.playback.get_volume().get(), 37)


class JsonRpcSingleNotificationTest(JsonRpcTestBase):
    def test_notification_does_not_return_a_result(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'core.get_uri_schemes',
        }
        response = self.jrw.handle_data(request)

        self.assertIsNone(response)

    def test_notification_makes_an_observable_change(self):
        self.assertEqual(self.core.playback.get_volume().get(), None)

        request = {
            'jsonrpc': '2.0',
            'method': 'core.playback.set_volume',
            'params': [37],
        }
        response = self.jrw.handle_data(request)

        self.assertIsNone(response)
        self.assertEqual(self.core.playback.get_volume().get(), 37)

    def test_notification_unknown_method_returns_nothing(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'bogus',
            'params': ['bogus'],
        }
        response = self.jrw.handle_data(request)

        self.assertIsNone(response)


class JsonRpcBatchTest(JsonRpcTestBase):
    def test_batch_of_only_commands_returns_all(self):
        self.core.tracklist.set_random(True).get()

        request = [
            {'jsonrpc': '2.0', 'method': 'core.tracklist.get_repeat', 'id': 1},
            {'jsonrpc': '2.0', 'method': 'core.tracklist.get_random', 'id': 2},
            {'jsonrpc': '2.0', 'method': 'core.tracklist.get_single', 'id': 3},
        ]
        response = self.jrw.handle_data(request)

        self.assertEqual(len(response), 3)

        response = dict((row['id'], row) for row in response)
        self.assertEqual(response[1]['result'], False)
        self.assertEqual(response[2]['result'], True)
        self.assertEqual(response[3]['result'], False)

    def test_batch_of_commands_and_notifications_returns_some(self):
        self.core.tracklist.set_random(True).get()

        request = [
            {'jsonrpc': '2.0', 'method': 'core.tracklist.get_repeat'},
            {'jsonrpc': '2.0', 'method': 'core.tracklist.get_random', 'id': 2},
            {'jsonrpc': '2.0', 'method': 'core.tracklist.get_single', 'id': 3},
        ]
        response = self.jrw.handle_data(request)

        self.assertEqual(len(response), 2)

        response = dict((row['id'], row) for row in response)
        self.assertNotIn(1, response)
        self.assertEqual(response[2]['result'], True)
        self.assertEqual(response[3]['result'], False)

    def test_batch_of_only_notifications_returns_nothing(self):
        self.core.tracklist.set_random(True).get()

        request = [
            {'jsonrpc': '2.0', 'method': 'core.tracklist.get_repeat'},
            {'jsonrpc': '2.0', 'method': 'core.tracklist.get_random'},
            {'jsonrpc': '2.0', 'method': 'core.tracklist.get_single'},
        ]
        response = self.jrw.handle_data(request)

        self.assertIsNone(response)


class JsonRpcSingleCommandErrorTest(JsonRpcTestBase):
    def test_application_error_response(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'calc.fail',
            'params': [],
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertNotIn('result', response)

        error = response['error']
        self.assertEqual(error['code'], 0)
        self.assertEqual(error['message'], 'Application error')

        data = error['data']
        self.assertEqual(data['type'], 'ValueError')
        self.assertIn('What did you expect?', data['message'])
        self.assertIn('traceback', data)
        self.assertIn('Traceback (most recent call last):', data['traceback'])

    def test_missing_jsonrpc_member_causes_invalid_request_error(self):
        request = {
            'method': 'core.get_uri_schemes',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertIsNone(response['id'])
        error = response['error']
        self.assertEqual(error['code'], -32600)
        self.assertEqual(error['message'], 'Invalid Request')
        self.assertEqual(error['data'], '"jsonrpc" member must be included')

    def test_wrong_jsonrpc_version_causes_invalid_request_error(self):
        request = {
            'jsonrpc': '3.0',
            'method': 'core.get_uri_schemes',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertIsNone(response['id'])
        error = response['error']
        self.assertEqual(error['code'], -32600)
        self.assertEqual(error['message'], 'Invalid Request')
        self.assertEqual(error['data'], '"jsonrpc" value must be "2.0"')

    def test_missing_method_member_causes_invalid_request_error(self):
        request = {
            'jsonrpc': '2.0',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertIsNone(response['id'])
        error = response['error']
        self.assertEqual(error['code'], -32600)
        self.assertEqual(error['message'], 'Invalid Request')
        self.assertEqual(error['data'], '"method" member must be included')

    def test_invalid_method_value_causes_invalid_request_error(self):
        request = {
            'jsonrpc': '2.0',
            'method': 1,
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertIsNone(response['id'])
        error = response['error']
        self.assertEqual(error['code'], -32600)
        self.assertEqual(error['message'], 'Invalid Request')
        self.assertEqual(error['data'], '"method" must be a string')

    def test_invalid_params_value_causes_invalid_request_error(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'core.get_uri_schemes',
            'params': 'foobar',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        self.assertIsNone(response['id'])
        error = response['error']
        self.assertEqual(error['code'], -32600)
        self.assertEqual(error['message'], 'Invalid Request')
        self.assertEqual(
            error['data'], '"params", if given, must be an array or an object')

    def test_method_on_without_object_causes_unknown_method_error(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'bogus',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        error = response['error']
        self.assertEqual(error['code'], -32601)
        self.assertEqual(error['message'], 'Method not found')
        self.assertEqual(
            error['data'],
            'Could not find object mount in method name "bogus"')

    def test_method_on_unknown_object_causes_unknown_method_error(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'bogus.bogus',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        error = response['error']
        self.assertEqual(error['code'], -32601)
        self.assertEqual(error['message'], 'Method not found')
        self.assertEqual(error['data'], 'No object found at "bogus"')

    def test_unknown_method_on_known_object_causes_unknown_method_error(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'core.bogus',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        error = response['error']
        self.assertEqual(error['code'], -32601)
        self.assertEqual(error['message'], 'Method not found')
        self.assertEqual(
            error['data'], 'Object mounted at "core" has no member "bogus"')

    def test_private_method_causes_unknown_method_error(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'core._secret',
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        error = response['error']
        self.assertEqual(error['code'], -32601)
        self.assertEqual(error['message'], 'Method not found')
        self.assertEqual(error['data'], 'Private methods are not exported')

    def test_invalid_params_causes_invalid_params_error(self):
        request = {
            'jsonrpc': '2.0',
            'method': 'core.get_uri_schemes',
            'params': ['bogus'],
            'id': 1,
        }
        response = self.jrw.handle_data(request)

        error = response['error']
        self.assertEqual(error['code'], -32602)
        self.assertEqual(error['message'], 'Invalid params')

        data = error['data']
        self.assertEqual(data['type'], 'TypeError')
        self.assertEqual(
            data['message'],
            'get_uri_schemes() takes exactly 1 argument (2 given)')
        self.assertIn('traceback', data)
        self.assertIn('Traceback (most recent call last):', data['traceback'])


class JsonRpcBatchErrorTest(JsonRpcTestBase):
    def test_empty_batch_list_causes_invalid_request_error(self):
        request = []
        response = self.jrw.handle_data(request)

        self.assertIsNone(response['id'])
        error = response['error']
        self.assertEqual(error['code'], -32600)
        self.assertEqual(error['message'], 'Invalid Request')
        self.assertEqual(error['data'], 'Batch list cannot be empty')

    def test_batch_with_invalid_command_causes_invalid_request_error(self):
        request = [1]
        response = self.jrw.handle_data(request)

        self.assertEqual(len(response), 1)
        response = response[0]
        self.assertIsNone(response['id'])
        error = response['error']
        self.assertEqual(error['code'], -32600)
        self.assertEqual(error['message'], 'Invalid Request')
        self.assertEqual(error['data'], 'Request must be an object')

    def test_batch_with_invalid_commands_causes_invalid_request_error(self):
        request = [1, 2, 3]
        response = self.jrw.handle_data(request)

        self.assertEqual(len(response), 3)
        response = response[2]
        self.assertIsNone(response['id'])
        error = response['error']
        self.assertEqual(error['code'], -32600)
        self.assertEqual(error['message'], 'Invalid Request')
        self.assertEqual(error['data'], 'Request must be an object')

    def test_batch_of_both_successfull_and_failing_requests(self):
        request = [
            # Call with positional params
            {'jsonrpc': '2.0', 'method': 'core.playback.set_volume',
                'params': [47], 'id': '1'},
            # Notification
            {'jsonrpc': '2.0', 'method': 'core.tracklist.set_consume',
                'params': [True]},
            # Call with positional params
            {'jsonrpc': '2.0', 'method': 'core.tracklist.set_repeat',
                'params': [False], 'id': '2'},
            # Invalid request
            {'foo': 'boo'},
            # Unknown method
            {'jsonrpc': '2.0', 'method': 'foo.get',
                'params': {'name': 'myself'}, 'id': '5'},
            # Call without params
            {'jsonrpc': '2.0', 'method': 'core.tracklist.get_random',
                'id': '9'},
        ]
        response = self.jrw.handle_data(request)

        self.assertEqual(len(response), 5)
        response = dict((row['id'], row) for row in response)
        self.assertEqual(response['1']['result'], None)
        self.assertEqual(response['2']['result'], None)
        self.assertEqual(response[None]['error']['code'], -32600)
        self.assertEqual(response['5']['error']['code'], -32601)
        self.assertEqual(response['9']['result'], False)


class JsonRpcInspectorTest(JsonRpcTestBase):
    def test_empty_object_mounts_is_not_allowed(self):
        test = lambda: jsonrpc.JsonRpcInspector(objects={'': Calculator})
        self.assertRaises(AttributeError, test)

    def test_can_describe_method_on_root(self):
        inspector = jsonrpc.JsonRpcInspector({
            'hello': lambda: 'Hello, world!',
        })

        methods = inspector.describe()

        self.assertIn('hello', methods)
        self.assertEqual(len(methods['hello']['params']), 0)

    def test_inspector_can_describe_an_object_with_methods(self):
        inspector = jsonrpc.JsonRpcInspector({
            'calc': Calculator,
        })

        methods = inspector.describe()

        self.assertIn('calc.add', methods)
        self.assertEqual(
            methods['calc.add']['description'],
            'Returns the sum of the given numbers')

        self.assertIn('calc.sub', methods)
        self.assertIn('calc.take_it_all', methods)
        self.assertNotIn('calc._secret', methods)
        self.assertNotIn('calc.__init__', methods)

        method = methods['calc.take_it_all']
        self.assertIn('params', method)

        params = method['params']

        self.assertEqual(params[0]['name'], 'a')
        self.assertNotIn('default', params[0])

        self.assertEqual(params[1]['name'], 'b')
        self.assertNotIn('default', params[1])

        self.assertEqual(params[2]['name'], 'c')
        self.assertEqual(params[2]['default'], True)

        self.assertEqual(params[3]['name'], 'args')
        self.assertNotIn('default', params[3])
        self.assertEqual(params[3]['varargs'], True)

        self.assertEqual(params[4]['name'], 'kwargs')
        self.assertNotIn('default', params[4])
        self.assertEqual(params[4]['kwargs'], True)

    def test_inspector_can_describe_a_bunch_of_large_classes(self):
        inspector = jsonrpc.JsonRpcInspector({
            'core.get_uri_schemes': core.Core.get_uri_schemes,
            'core.library': core.LibraryController,
            'core.playback': core.PlaybackController,
            'core.playlists': core.PlaylistsController,
            'core.tracklist':  core.TracklistController,
        })

        methods = inspector.describe()

        self.assertIn('core.get_uri_schemes', methods)
        self.assertEquals(len(methods['core.get_uri_schemes']['params']), 0)

        self.assertIn('core.library.lookup', methods.keys())
        self.assertEquals(
            methods['core.library.lookup']['params'][0]['name'], 'uri')

        self.assertIn('core.playback.next', methods)
        self.assertEquals(len(methods['core.playback.next']['params']), 0)

        self.assertIn('core.playlists.get_playlists', methods)
        self.assertEquals(
            len(methods['core.playlists.get_playlists']['params']), 1)

        self.assertIn('core.tracklist.filter', methods.keys())
        self.assertEquals(
            methods['core.tracklist.filter']['params'][0]['name'], 'criteria')
        self.assertEquals(
            methods['core.tracklist.filter']['params'][1]['name'], 'kwargs')
        self.assertEquals(
            methods['core.tracklist.filter']['params'][1]['kwargs'], True)
