import unittest
from six import iterkeys

from mongoengine import Document
from mongoengine.base.datastructures import StrictDict, BaseList, BaseDict


class DocumentStub(object):
    def __init__(self):
        self._changed_fields = []

    def _mark_as_changed(self, key):
        self._changed_fields.append(key)


class TestBaseDict(unittest.TestCase):

    @staticmethod
    def _get_basedict(dict_items):
        """Get a BaseList bound to a fake document instance"""
        fake_doc = DocumentStub()
        base_list = BaseDict(dict_items, instance=None, name='my_name')
        base_list._instance = fake_doc  # hack to inject the mock, it does not work in the constructor
        return base_list

    def test___init___(self):
        class MyDoc(Document):
            pass

        dict_items = {'k': 'v'}
        doc = MyDoc()
        base_dict = BaseDict(dict_items, instance=doc, name='my_name')
        self.assertIsInstance(base_dict._instance, Document)
        self.assertEqual(base_dict._name, 'my_name')
        self.assertEqual(base_dict, dict_items)

    def test_setdefault_calls_mark_as_changed(self):
        base_dict = self._get_basedict({})
        base_dict.setdefault('k', 'v')
        self.assertEqual(base_dict._instance._changed_fields, [base_dict._name])

    def test_popitems_calls_mark_as_changed(self):
        base_dict = self._get_basedict({'k': 'v'})
        self.assertEqual(base_dict.popitem(), ('k', 'v'))
        self.assertEqual(base_dict._instance._changed_fields, [base_dict._name])
        self.assertFalse(base_dict)

    def test_pop_calls_mark_as_changed(self):
        base_dict = self._get_basedict({'k': 'v'})
        self.assertEqual(base_dict.pop('k'), 'v')
        self.assertEqual(base_dict._instance._changed_fields, [base_dict._name])
        self.assertFalse(base_dict)

    def test_pop_calls_does_not_mark_as_changed_when_it_fails(self):
        base_dict = self._get_basedict({'k': 'v'})
        with self.assertRaises(KeyError):
            base_dict.pop('X')
        self.assertFalse(base_dict._instance._changed_fields)

    def test_clear_calls_mark_as_changed(self):
        base_dict = self._get_basedict({'k': 'v'})
        base_dict.clear()
        self.assertEqual(base_dict._instance._changed_fields, ['my_name'])
        self.assertEqual(base_dict, {})

    def test___delitem___calls_mark_as_changed(self):
        base_dict = self._get_basedict({'k': 'v'})
        del base_dict['k']
        self.assertEqual(base_dict._instance._changed_fields, ['my_name.k'])
        self.assertEqual(base_dict, {})

    def test___getitem____KeyError(self):
        base_dict = self._get_basedict({})
        with self.assertRaises(KeyError):
            base_dict['new']

    def test___getitem____simple_value(self):
        base_dict = self._get_basedict({'k': 'v'})
        base_dict['k'] = 'v'

    def test___getitem____sublist_gets_converted_to_BaseList(self):
        base_dict = self._get_basedict({'k': [0, 1, 2]})
        sub_list = base_dict['k']
        self.assertEqual(sub_list, [0, 1, 2])
        self.assertIsInstance(sub_list, BaseList)
        self.assertIs(sub_list._instance, base_dict._instance)
        self.assertEqual(sub_list._name, 'my_name.k')
        self.assertEqual(base_dict._instance._changed_fields, [])

        # Challenge mark_as_changed from sublist
        sub_list[1] = None
        self.assertEqual(base_dict._instance._changed_fields, ['my_name.k.1'])

    def test___getitem____subdict_gets_converted_to_BaseDict(self):
        base_dict = self._get_basedict({'k': {'subk': 'subv'}})
        sub_dict = base_dict['k']
        self.assertEqual(sub_dict, {'subk': 'subv'})
        self.assertIsInstance(sub_dict, BaseDict)
        self.assertIs(sub_dict._instance, base_dict._instance)
        self.assertEqual(sub_dict._name, 'my_name.k')
        self.assertEqual(base_dict._instance._changed_fields, [])

        # Challenge mark_as_changed from subdict
        sub_dict['subk'] = None
        self.assertEqual(base_dict._instance._changed_fields, ['my_name.k.subk'])

    def test_get_sublist_gets_converted_to_BaseList_just_like__getitem__(self):
        base_dict = self._get_basedict({'k': [0, 1, 2]})
        sub_list = base_dict.get('k')
        self.assertEqual(sub_list, [0, 1, 2])
        self.assertIsInstance(sub_list, BaseList)

    def test_get_returns_the_same_as___getitem__(self):
        base_dict = self._get_basedict({'k': [0, 1, 2]})
        get_ = base_dict.get('k')
        getitem_ = base_dict['k']
        self.assertEqual(get_, getitem_)

    def test_get_default(self):
        base_dict = self._get_basedict({})
        sentinel = object()
        self.assertEqual(base_dict.get('new'), None)
        self.assertIs(base_dict.get('new', sentinel), sentinel)

    def test___setitem___calls_mark_as_changed(self):
        base_dict = self._get_basedict({})
        base_dict['k'] = 'v'
        self.assertEqual(base_dict._instance._changed_fields, ['my_name.k'])
        self.assertEqual(base_dict, {'k': 'v'})

    def test_update_calls_mark_as_changed(self):
        base_dict = self._get_basedict({})
        base_dict.update({'k': 'v'})
        self.assertEqual(base_dict._instance._changed_fields, ['my_name'])

    def test___setattr____not_tracked_by_changes(self):
        base_dict = self._get_basedict({})
        base_dict.a_new_attr = 'test'
        self.assertEqual(base_dict._instance._changed_fields, [])

    def test___delattr____tracked_by_changes(self):
        # This is probably a bug as __setattr__ is not tracked
        # This is even bad because it could be that there is an attribute
        # with the same name as a key
        base_dict = self._get_basedict({})
        base_dict.a_new_attr = 'test'
        del base_dict.a_new_attr
        self.assertEqual(base_dict._instance._changed_fields, ['my_name.a_new_attr'])


class TestBaseList(unittest.TestCase):

    @staticmethod
    def _get_baselist(list_items):
        """Get a BaseList bound to a fake document instance"""
        fake_doc = DocumentStub()
        base_list = BaseList(list_items, instance=None, name='my_name')
        base_list._instance = fake_doc  # hack to inject the mock, it does not work in the constructor
        return base_list

    def test___init___(self):
        class MyDoc(Document):
            pass

        list_items = [True]
        doc = MyDoc()
        base_list = BaseList(list_items, instance=doc, name='my_name')
        self.assertIsInstance(base_list._instance, Document)
        self.assertEqual(base_list._name, 'my_name')
        self.assertEqual(base_list, list_items)

    def test___iter__(self):
        values = [True, False, True, False]
        base_list = BaseList(values, instance=None, name='my_name')
        self.assertEqual(values, list(base_list))

    def test___iter___allow_modification_while_iterating_withou_error(self):
        # regular list allows for this, thus this subclass must comply to that
        base_list = BaseList([True, False, True, False], instance=None, name='my_name')
        for idx, val in enumerate(base_list):
            if val:
                base_list.pop(idx)

    def test_append_calls_mark_as_changed(self):
        base_list = self._get_baselist([])
        self.assertFalse(base_list._instance._changed_fields)
        base_list.append(True)
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])

    def test_subclass_append(self):
        # Due to the way mark_as_changed_wrapper is implemented
        # it is good to test subclasses
        class SubBaseList(BaseList):
            pass

        base_list = SubBaseList([], instance=None, name='my_name')
        base_list.append(True)

    def test___getitem__using_simple_index(self):
        base_list = self._get_baselist([0, 1, 2])
        self.assertEqual(base_list[0], 0)
        self.assertEqual(base_list[1], 1)
        self.assertEqual(base_list[-1], 2)

    def test___getitem__using_slice(self):
        base_list = self._get_baselist([0, 1, 2])
        self.assertEqual(base_list[1:3], [1, 2])
        self.assertEqual(base_list[0:3:2], [0, 2])

    def test___getitem___using_slice_returns_list(self):
        # Bug: using slice does not properly handles the instance
        # and mark_as_changed behaviour.
        base_list = self._get_baselist([0, 1, 2])
        sliced = base_list[1:3]
        self.assertEqual(sliced, [1, 2])
        self.assertIsInstance(sliced, list)
        self.assertEqual(base_list._instance._changed_fields, [])

    def test___getitem__sublist_returns_BaseList_bound_to_instance(self):
        base_list = self._get_baselist(
            [
                [1, 2],
                [3, 4]
            ]
        )
        sub_list = base_list[0]
        self.assertEqual(sub_list, [1, 2])
        self.assertIsInstance(sub_list, BaseList)
        self.assertIs(sub_list._instance, base_list._instance)
        self.assertEqual(sub_list._name, 'my_name.0')
        self.assertEqual(base_list._instance._changed_fields, [])

        # Challenge mark_as_changed from sublist
        sub_list[1] = None
        self.assertEqual(base_list._instance._changed_fields, ['my_name.0.1'])

    def test___getitem__subdict_returns_BaseList_bound_to_instance(self):
        base_list = self._get_baselist(
            [
                {'subk': 'subv'}
            ]
        )
        sub_dict = base_list[0]
        self.assertEqual(sub_dict, {'subk': 'subv'})
        self.assertIsInstance(sub_dict, BaseDict)
        self.assertIs(sub_dict._instance, base_list._instance)
        self.assertEqual(sub_dict._name, 'my_name.0')
        self.assertEqual(base_list._instance._changed_fields, [])

        # Challenge mark_as_changed from subdict
        sub_dict['subk'] = None
        self.assertEqual(base_list._instance._changed_fields, ['my_name.0.subk'])

    def test_extend_calls_mark_as_changed(self):
        base_list = self._get_baselist([])
        base_list.extend([True])
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])

    def test_insert_calls_mark_as_changed(self):
        base_list = self._get_baselist([])
        base_list.insert(0, True)
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])

    def test_remove_calls_mark_as_changed(self):
        base_list = self._get_baselist([True])
        base_list.remove(True)
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])

    def test_remove_not_mark_as_changed_when_it_fails(self):
        base_list = self._get_baselist([True])
        with self.assertRaises(ValueError):
            base_list.remove(False)
        self.assertFalse(base_list._instance._changed_fields)

    def test_pop_calls_mark_as_changed(self):
        base_list = self._get_baselist([True])
        base_list.pop()
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])

    def test_reverse_calls_mark_as_changed(self):
        base_list = self._get_baselist([True, False])
        base_list.reverse()
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])

    def test___delitem___calls_mark_as_changed(self):
        base_list = self._get_baselist([True])
        del base_list[0]
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])

    def test___setitem___calls_with_full_slice_mark_as_changed(self):
        base_list = self._get_baselist([])
        base_list[:] = [0, 1]      # Will use __setslice__ under py2 and __setitem__ under py3
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])
        self.assertEqual(base_list, [0, 1])

    def test___setitem___calls_with_partial_slice_mark_as_changed(self):
        base_list = self._get_baselist([0, 1, 2])
        base_list[0:2] = [1, 0]     # Will use __setslice__ under py2 and __setitem__ under py3
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])
        self.assertEqual(base_list, [1, 0, 2])

    def test___setitem___calls_with_step_slice_mark_as_changed(self):
        base_list = self._get_baselist([0, 1, 2])
        base_list[0:3:2] = [-1, -2]   # uses __setitem__ in both py2 & 3
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])
        self.assertEqual(base_list, [-1, 1, -2])

    def test___setitem___with_slice(self):
        base_list = self._get_baselist([0, 1, 2, 3, 4, 5])
        base_list[0:6:2] = [None, None, None]
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])
        self.assertEqual(base_list, [None, 1, None, 3, None, 5])

    def test___setitem___item_0_calls_mark_as_changed(self):
        base_list = self._get_baselist([True])
        base_list[0] = False
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])
        self.assertEqual(base_list, [False])

    def test___setitem___item_1_calls_mark_as_changed(self):
        base_list = self._get_baselist([True, True])
        base_list[1] = False
        self.assertEqual(base_list._instance._changed_fields, ['my_name.1'])
        self.assertEqual(base_list, [True, False])

    def test___delslice___calls_mark_as_changed(self):
        base_list = self._get_baselist([0, 1])
        del base_list[0:1]
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])
        self.assertEqual(base_list, [1])

    def test___iadd___calls_mark_as_changed(self):
        base_list = self._get_baselist([True])
        base_list += [False]
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])

    def test___imul___calls_mark_as_changed(self):
        base_list = self._get_baselist([True])
        self.assertEqual(base_list._instance._changed_fields, [])
        base_list *= 2
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])

    def test_sort_calls_not_marked_as_changed_when_it_fails(self):
        base_list = self._get_baselist([True])
        with self.assertRaises(TypeError):
            base_list.sort(key=1)

        self.assertEqual(base_list._instance._changed_fields, [])

    def test_sort_calls_mark_as_changed(self):
        base_list = self._get_baselist([True, False])
        base_list.sort()
        self.assertEqual(base_list._instance._changed_fields, ['my_name'])

    def test_sort_calls_with_key(self):
        base_list = self._get_baselist([1, 2, 11])
        base_list.sort(key=lambda i: str(i))
        self.assertEqual(base_list, [1, 11, 2])


class TestStrictDict(unittest.TestCase):
    def strict_dict_class(self, *args, **kwargs):
        return StrictDict.create(*args, **kwargs)

    def setUp(self):
        self.dtype = self.strict_dict_class(("a", "b", "c"))

    def test_init(self):
        d = self.dtype(a=1, b=1, c=1)
        self.assertEqual((d.a, d.b, d.c), (1, 1, 1))

    def test_iterkeys(self):
        d = self.dtype(a=1)
        self.assertEqual(list(iterkeys(d)), ['a'])

    def test_len(self):
        d = self.dtype(a=1)
        self.assertEqual(len(d), 1)

    def test_pop(self):
        d = self.dtype(a=1)
        self.assertIn('a', d)
        d.pop('a')
        self.assertNotIn('a', d)

    def test_repr(self):
        d = self.dtype(a=1, b=2, c=3)
        self.assertEqual(repr(d), '{"a": 1, "b": 2, "c": 3}')

        # make sure quotes are escaped properly
        d = self.dtype(a='"', b="'", c="")
        self.assertEqual(repr(d), '{"a": \'"\', "b": "\'", "c": \'\'}')

    def test_init_fails_on_nonexisting_attrs(self):
        with self.assertRaises(AttributeError):
            self.dtype(a=1, b=2, d=3)

    def test_eq(self):
        d = self.dtype(a=1, b=1, c=1)
        dd = self.dtype(a=1, b=1, c=1)
        e = self.dtype(a=1, b=1, c=3)
        f = self.dtype(a=1, b=1)
        g = self.strict_dict_class(("a", "b", "c", "d"))(a=1, b=1, c=1, d=1)
        h = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=1)
        i = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=2)

        self.assertEqual(d, dd)
        self.assertNotEqual(d, e)
        self.assertNotEqual(d, f)
        self.assertNotEqual(d, g)
        self.assertNotEqual(f, d)
        self.assertEqual(d, h)
        self.assertNotEqual(d, i)

    def test_setattr_getattr(self):
        d = self.dtype()
        d.a = 1
        self.assertEqual(d.a, 1)
        self.assertRaises(AttributeError, getattr, d, 'b')

    def test_setattr_raises_on_nonexisting_attr(self):
        d = self.dtype()
        with self.assertRaises(AttributeError):
            d.x = 1

    def test_setattr_getattr_special(self):
        d = self.strict_dict_class(["items"])
        d.items = 1
        self.assertEqual(d.items, 1)

    def test_get(self):
        d = self.dtype(a=1)
        self.assertEqual(d.get('a'), 1)
        self.assertEqual(d.get('b', 'bla'), 'bla')

    def test_items(self):
        d = self.dtype(a=1)
        self.assertEqual(list(d.items()), [('a', 1)])
        d = self.dtype(a=1, b=2)
        self.assertEqual(list(d.items()), [('a', 1), ('b', 2)])

    def test_mappings_protocol(self):
        d = self.dtype(a=1, b=2)
        self.assertEqual(dict(d), {'a': 1, 'b': 2})
        self.assertEqual(dict(**d), {'a': 1, 'b': 2})


if __name__ == '__main__':
    unittest.main()
