# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2014 Eugene Frolov <eugene@frolov.net.ru>
#
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import mock

from restalchemy.common import exceptions
from restalchemy.dm import properties
from restalchemy.dm import types
from restalchemy.tests.unit import base

FAKE_VALUE = 'FAKE_VALUE'
FAKE_VALUE2 = 'FAKE_VALUE2'
FAKE_VALUE3 = 'FAKE_VALUE3'


class PropertyTestCase(base.BaseTestCase):

    def setUp(self):
        super(PropertyTestCase, self).setUp()
        self.positive_fake_property_type = mock.MagicMock(
            **{'validate': mock.MagicMock(return_value=True),
               'spec': types.BaseType})
        self.negative_fake_property_type = mock.MagicMock(
            **{'validate': mock.MagicMock(return_value=False),
               'spec': types.BaseType})

    def _set_property_value(self, obj, value):
        obj.value = value

    def test_init_with_correct_value(self):
        property_obj = properties.Property(
            self.positive_fake_property_type, value=FAKE_VALUE)

        self.assertEqual(property_obj._value, FAKE_VALUE)
        self.positive_fake_property_type.validate.assert_called_with(
            FAKE_VALUE)

    def test_init_incorrect_value(self):
        self.assertRaises(exceptions.TypeError, properties.Property,
                          self.negative_fake_property_type, value=FAKE_VALUE)

    def test_init_default_value(self):
        property_obj = properties.Property(
            self.positive_fake_property_type, default=FAKE_VALUE)

        self.assertEqual(property_obj._value, FAKE_VALUE)
        self.positive_fake_property_type.validate.assert_called_with(
            FAKE_VALUE)

    def test_init_default_callable_value(self):
        property_obj = properties.Property(
            self.positive_fake_property_type, default=lambda: FAKE_VALUE)

        self.assertEqual(property_obj.value, FAKE_VALUE)
        self.positive_fake_property_type.validate.assert_called_with(
            FAKE_VALUE)

    def test_init_default_value_if_property_read_only(self):
        property_obj = properties.Property(
            self.positive_fake_property_type, default=FAKE_VALUE,
            read_only=True)

        self.assertEqual(property_obj._value, FAKE_VALUE)
        self.positive_fake_property_type.validate.assert_called_with(
            FAKE_VALUE)

    def test_init_none_and_require(self):
        self.assertRaises(exceptions.PropertyRequired, properties.Property,
                          self.negative_fake_property_type, required=True,
                          value=None)

    def test_init_value_is_none_default_is_not_none_and_prop_required(self):
        property_obj = properties.Property(
            self.positive_fake_property_type, value=None, required=True,
            default=FAKE_VALUE)

        self.assertEqual(property_obj._value, FAKE_VALUE)
        self.positive_fake_property_type.validate.assert_called_with(
            FAKE_VALUE)

    def test_init_none_value(self):
        property_obj = properties.Property(
            self.negative_fake_property_type, value=None)

        self.assertEqual(property_obj._value, None)

    def test_init_value_with_read_only(self):
        property_obj = properties.Property(
            self.positive_fake_property_type, read_only=True,
            value=FAKE_VALUE2)

        self.assertEqual(property_obj._value, FAKE_VALUE2)
        self.positive_fake_property_type.validate.assert_called_with(
            FAKE_VALUE2)

    def test_set_correct_value(self):
        property_obj = properties.Property(self.positive_fake_property_type)

        self.assertIsNone(self._set_property_value(property_obj, FAKE_VALUE))
        self.assertEqual(property_obj._value, FAKE_VALUE)
        self.positive_fake_property_type.validate.assert_called_with(
            FAKE_VALUE)

    def test_set_incorect_value(self):
        property_obj = properties.Property(self.negative_fake_property_type)
        old_value = property_obj._value

        self.assertRaises(exceptions.TypeError, self._set_property_value,
                          property_obj, FAKE_VALUE)
        self.assertEqual(property_obj._value, old_value)
        self.negative_fake_property_type.validate.assert_called_with(
            FAKE_VALUE)

    def test_set_value_if_property_read_only(self):
        property_obj = properties.Property(
            self.positive_fake_property_type, read_only=True)

        self.assertRaises(exceptions.ReadOnlyProperty,
                          self._set_property_value, property_obj, FAKE_VALUE2)
        self.assertEqual(property_obj._value, None)

    def test_set_force_correct_value(self):
        property_obj = properties.Property(self.positive_fake_property_type)

        self.assertIsNone(property_obj.set_value_force(FAKE_VALUE))
        self.assertEqual(property_obj._value, FAKE_VALUE)
        self.positive_fake_property_type.validate.assert_called_with(
            FAKE_VALUE)

    def test_set_force_incorect_value(self):
        property_obj = properties.Property(self.negative_fake_property_type)
        old_value = property_obj._value

        self.assertRaises(exceptions.TypeError, property_obj.set_value_force,
                          FAKE_VALUE)
        self.assertEqual(property_obj._value, old_value)
        self.negative_fake_property_type.validate.assert_called_with(
            FAKE_VALUE)

    def test_set_force_none_value(self):
        property_obj = properties.Property(self.positive_fake_property_type,
                                           required=True, value=FAKE_VALUE)
        old_value = property_obj._value

        self.assertRaises(exceptions.PropertyRequired,
                          property_obj.set_value_force, None)
        self.assertEqual(property_obj._value, old_value)


class PropertyCreatorTestCase(base.BaseTestCase):

    ARGS = [1, 2, 3]
    KWARGS = {'fake_key1': 'fake_value1', 'fake_key2': 'fake_value2'}

    def setUp(self):
        self.property_mock = mock.Mock(return_value=FAKE_VALUE)
        self.prop_type_mock = mock.Mock()
        self.test_instance = properties.PropertyCreator(self.property_mock,
                                                        self.prop_type_mock,
                                                        self.ARGS, self.KWARGS)

    def test_call_object(self):
        self.assertEqual(self.test_instance(FAKE_VALUE2), FAKE_VALUE)
        self.property_mock.assert_called_once_with(
            value=FAKE_VALUE2,
            property_type=self.prop_type_mock,
            *self.ARGS, **self.KWARGS)

    def test_get_property_class(self):
        self.assertEqual(self.test_instance.get_property_class(),
                         self.property_mock)


class PropertyCollectionTestCase(base.BaseTestCase):

    def setUp(self):

        self.kwargs = {
            'fake1': mock.Mock(
                **{'get_property_class.return_value': FAKE_VALUE,
                   'return_value': FAKE_VALUE2}),
            'fake2': mock.Mock(
                **{'get_property_class.return_value': FAKE_VALUE2,
                   'return_value': FAKE_VALUE})
        }
        self.test_instance = properties.PropertyCollection(**self.kwargs)

    def test_properties(self):
        self.assertEqual(self.test_instance.properties, self.kwargs)

    def test_change_properties_dict(self):
        def set_item(d, key, value):
            d[key] = value

        self.assertRaises(TypeError, set_item, self.test_instance.properties,
                          'fake1', 2)

    def test_get_item_fake1(self):
        self.assertEqual(self.test_instance['fake1'], FAKE_VALUE)
        self.assertTrue(self.kwargs['fake1'].get_property_class.called)
        self.assertFalse(self.kwargs['fake2'].get_property_class.called)

    def test_get_item_fake2(self):
        self.assertEqual(self.test_instance['fake2'], FAKE_VALUE2)
        self.assertFalse(self.kwargs['fake1'].get_property_class.called)
        self.assertTrue(self.kwargs['fake2'].get_property_class.called)

    def test_instantiate_property_fake1(self):
        self.assertEqual(self.test_instance.instantiate_property(
            name='fake1', value=FAKE_VALUE3), FAKE_VALUE2)
        self.kwargs['fake1'].assert_called_once_with(FAKE_VALUE3)
        self.assertFalse(self.kwargs['fake2'].called)

    def test_instantiate_property_fake2(self):
        self.assertEqual(self.test_instance.instantiate_property(
            name='fake2', value=FAKE_VALUE3), FAKE_VALUE)
        self.kwargs['fake2'].assert_called_once_with(FAKE_VALUE3)
        self.assertFalse(self.kwargs['fake1'].called)

    def test_concatenate_two_collections(self):
        fake_property3 = mock.Mock()
        test_instance2 = properties.PropertyCollection(fake3=fake_property3)
        new_properties = self.kwargs
        new_properties['fake3'] = fake_property3

        res = self.test_instance + test_instance2
        self.assertIsInstance(res, properties.PropertyCollection)
        self.assertNotEqual(res, self.test_instance)
        self.assertNotEqual(res, test_instance2)
        self.assertEqual(res._properties, new_properties)

    def test_concatenate_collection_and_other_object(self):

        def concatenate(a, b):
            return a + b

        self.assertRaises(TypeError, concatenate, self.test_instance, object())


class PropertyManagerTestCase(base.BaseTestCase):

    def setUp(self):
        self.collection_mock = mock.Mock(**{
            'properties.items.return_value': [
                ('fake1', 'fake1'),
                ('fake2', 'fake2')
            ],
            'instantiate_property.return_value': FAKE_VALUE})

    def test_init_manager(self):
        res = properties.PropertyManager(self.collection_mock)
        instantiate_property_calls = [
            mock.call('fake1', None),
            mock.call('fake2', None)]

        self.assertIsInstance(res, properties.PropertyManager)
        self.collection_mock.instantiate_property.assert_has_calls(
            instantiate_property_calls, any_order=True)

    def test_init_manager_with_correct_kwargs(self):
        res = properties.PropertyManager(self.collection_mock,
                                         fake1=FAKE_VALUE2,
                                         fake2=FAKE_VALUE3)
        instantiate_property_calls = [
            mock.call('fake1', FAKE_VALUE2),
            mock.call('fake2', FAKE_VALUE3)]

        self.assertIsInstance(res, properties.PropertyManager)
        self.collection_mock.instantiate_property.assert_has_calls(
            instantiate_property_calls, any_order=True)

    @base.unittest.skip("Checking of redundant data is turned off.")
    def test_init_manager_with_incorrect_kwargs(self):
        self.assertRaises(TypeError, properties.PropertyManager,
                          self.collection_mock, fake3=FAKE_VALUE3)

    def test_properties(self):
        property_manager = properties.PropertyManager(self.collection_mock)

        self.assertEqual(property_manager.properties, {'fake1': 'FAKE_VALUE',
                                                       'fake2': 'FAKE_VALUE'})

    def test_change_properties_dict(self):
        property_manager = properties.PropertyManager(self.collection_mock)

        def set_item(d, key, value):
            d[key] = value

        self.assertRaises(TypeError, set_item, property_manager.properties,
                          'fake1', 2)


@mock.patch('restalchemy.dm.properties.PropertyCreator',
            return_value=FAKE_VALUE)
class PropertyFuncTestCase(base.BaseTestCase):

    ARGS = (1, 2, 3)
    KWARGS = {'fake_key1': 'fake_value1', 'fake_key2': 'fake_value2'}

    def test_create_property(self, pc_mock):
        self.assertEqual(properties.property(*self.ARGS, **self.KWARGS),
                         FAKE_VALUE)
        pc_mock.assert_called_once_with(
            prop_class=properties.Property, prop_type=1, args=self.ARGS[1:],
            kwargs=self.KWARGS)

    def test_create_property_with_property_class(self, pc_mock):

        class LocalProperty(properties.AbstractProperty):

            @property
            def value(self):
                pass

            def set_value_force(self, value):
                pass

        self.assertEqual(properties.property(
            property_class=LocalProperty, *self.ARGS, **self.KWARGS),
            FAKE_VALUE)
        pc_mock.assert_called_once_with(
            prop_class=LocalProperty, prop_type=1, args=self.ARGS[1:],
            kwargs=self.KWARGS)

    def test_create_property_with_incorect_property_class(self, pc_mock):
        self.assertRaises(ValueError, properties.property,
                          property_class=object, *self.ARGS, **self.KWARGS)
