from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.test import TestCase

from misago.core.apipatch import ApiPatch, InvalidAction


class MockRequest(object):
    def __init__(self, data=None):
        self.data = data


class MockObject(object):
    def __init__(self, pk):
        self.id = pk
        self.pk = pk


class ApiPatchTests(TestCase):
    def test_add(self):
        """add method adds function to patch object"""
        patch = ApiPatch()

        def mock_function():
            pass

        patch.add('test-add', mock_function)

        self.assertEqual(len(patch._actions), 1)
        self.assertEqual(patch._actions[0]['op'], 'add')
        self.assertEqual(patch._actions[0]['path'], 'test-add')
        self.assertEqual(patch._actions[0]['handler'], mock_function)

    def test_remove(self):
        """remove method adds function to patch object"""
        patch = ApiPatch()

        def mock_function():
            pass

        patch.remove('test-remove', mock_function)

        self.assertEqual(len(patch._actions), 1)
        self.assertEqual(patch._actions[0]['op'], 'remove')
        self.assertEqual(patch._actions[0]['path'], 'test-remove')
        self.assertEqual(patch._actions[0]['handler'], mock_function)

    def test_replace(self):
        """replace method adds function to patch object"""
        patch = ApiPatch()

        def mock_function():
            pass

        patch.replace('test-replace', mock_function)

        self.assertEqual(len(patch._actions), 1)
        self.assertEqual(patch._actions[0]['op'], 'replace')
        self.assertEqual(patch._actions[0]['path'], 'test-replace')
        self.assertEqual(patch._actions[0]['handler'], mock_function)

    def test_validate_action(self):
        """validate_action method validates action dict"""
        patch = ApiPatch()

        VALID_ACTIONS = [
            {
                'op': 'add',
                'path': 'test',
                'value': 42
            },
            {
                'op': 'remove',
                'path': 'other-test',
                'value': 'Lorem'
            },
            {
                'op': 'replace',
                'path': 'false-test',
                'value': None
            },
        ]

        for action in VALID_ACTIONS:
            patch.validate_action(action)

        # undefined op
        UNSUPPORTED_ACTIONS = ({}, {'op': ''}, {'no': 'op'}, )

        for action in UNSUPPORTED_ACTIONS:
            try:
                patch.validate_action(action)
            except InvalidAction as e:
                self.assertEqual(e.args[0], u"undefined op")

        # unsupported op
        try:
            patch.validate_action({'op': 'nope'})
        except InvalidAction as e:
            self.assertEqual(e.args[0], u'"nope" op is unsupported')

        # op lacking patch
        try:
            patch.validate_action({'op': 'add'})
        except InvalidAction as e:
            self.assertEqual(e.args[0], u'"add" op has to specify path')

        # op lacking value
        try:
            patch.validate_action({
                'op': 'add',
                'path': 'yolo',
            })
        except InvalidAction as e:
            self.assertEqual(e.args[0], u'"add" op has to specify value')

        # empty value is allowed
        try:
            patch.validate_action({
                'op': 'add',
                'path': 'yolo',
                'value': '',
            })
        except InvalidAction as e:
            self.assertEqual(e.args[0], u'"add" op has to specify value')

    def test_dispatch_action(self):
        """dispatch_action calls specified actions"""
        patch = ApiPatch()

        mock_target = MockObject(13)

        def action_a(request, target, value):
            self.assertEqual(request, 'request')
            self.assertEqual(target, mock_target)
            return {'a': value * 2, 'b': 111}

        patch.replace('abc', action_a)

        def action_b(request, target, value):
            self.assertEqual(request, 'request')
            self.assertEqual(target, mock_target)
            return {'b': value * 10}

        patch.replace('abc', action_b)

        def action_fail(request, target, value):
            self.fail("unrequired action was called")

        patch.add('c', action_fail)
        patch.remove('c', action_fail)
        patch.replace('c', action_fail)

        patch_dict = {'id': 123}

        patch.dispatch_action(
            patch_dict, 'request', mock_target, {
                'op': 'replace',
                'path': 'abc',
                'value': 5,
            }
        )

        self.assertEqual(len(patch_dict), 3)
        self.assertEqual(patch_dict['id'], 123)
        self.assertEqual(patch_dict['a'], 10)
        self.assertEqual(patch_dict['b'], 50)

    def test_dispatch(self):
        """dispatch calls actions and returns response"""
        patch = ApiPatch()

        def action_error(request, target, value):
            if value == '404':
                raise Http404()
            if value == 'perm':
                raise PermissionDenied("yo ain't doing that!")

        patch.replace('error', action_error)

        def action_mutate(request, target, value):
            return {'value': value * 2}

        patch.replace('mutate', action_mutate)

        # dispatch requires list as an argument
        response = patch.dispatch(MockRequest({}), {})
        self.assertEqual(response.status_code, 400)

        self.assertEqual(response.data['detail'], "PATCH request should be list of operations")

        # valid dispatch
        response = patch.dispatch(
            MockRequest([
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 2,
                },
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 6,
                },
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 7,
                },
            ]), MockObject(13)
        )

        self.assertEqual(response.status_code, 200)

        self.assertEqual(len(response.data['detail']), 3)
        self.assertEqual(response.data['detail'][0], 'ok')
        self.assertEqual(response.data['detail'][1], 'ok')
        self.assertEqual(response.data['detail'][2], 'ok')
        self.assertEqual(response.data['id'], 13)
        self.assertEqual(response.data['value'], 14)

        # invalid action in dispatch
        response = patch.dispatch(
            MockRequest([
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 2,
                },
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 6,
                },
                {
                    'op': 'replace',
                },
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 7,
                },
            ]), MockObject(13)
        )

        self.assertEqual(response.status_code, 400)

        self.assertEqual(len(response.data['detail']), 3)
        self.assertEqual(response.data['detail'][0], 'ok')
        self.assertEqual(response.data['detail'][1], 'ok')
        self.assertEqual(response.data['detail'][2], '"replace" op has to specify path')
        self.assertEqual(response.data['id'], 13)
        self.assertEqual(response.data['value'], 12)

        # action in dispatch raised 404
        response = patch.dispatch(
            MockRequest([
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 2,
                },
                {
                    'op': 'replace',
                    'path': 'error',
                    'value': '404',
                },
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 6,
                },
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 7,
                },
            ]), MockObject(13)
        )

        self.assertEqual(response.status_code, 400)

        self.assertEqual(len(response.data['detail']), 2)
        self.assertEqual(response.data['detail'][0], 'ok')
        self.assertEqual(response.data['detail'][1], "NOT FOUND")
        self.assertEqual(response.data['id'], 13)
        self.assertEqual(response.data['value'], 4)

        # action in dispatch raised perm denied
        response = patch.dispatch(
            MockRequest([
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 2,
                },
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 6,
                },
                {
                    'op': 'replace',
                    'path': 'mutate',
                    'value': 9,
                },
                {
                    'op': 'replace',
                    'path': 'error',
                    'value': 'perm',
                },
            ]), MockObject(13)
        )

        self.assertEqual(response.status_code, 400)

        self.assertEqual(len(response.data['detail']), 4)
        self.assertEqual(response.data['detail'][0], 'ok')
        self.assertEqual(response.data['detail'][1], 'ok')
        self.assertEqual(response.data['detail'][2], 'ok')
        self.assertEqual(response.data['detail'][3], "yo ain't doing that!")
        self.assertEqual(response.data['id'], 13)
        self.assertEqual(response.data['value'], 18)
