import factory
import json
from mock import Mock
from django.contrib.auth.models import User

from functools import partial

from courseware.model_data import LmsKeyValueStore, InvalidWriteError, InvalidScopeError, ModelDataCache
from courseware.models import StudentModule, XModuleContentField, XModuleSettingsField, XModuleStudentInfoField, XModuleStudentPrefsField
from xblock.core import Scope, BlockScope
from xmodule.modulestore import Location

from django.test import TestCase


def mock_field(scope, name):
    field = Mock()
    field.scope = scope
    field.name = name
    return field

def mock_descriptor(fields=[], lms_fields=[]):
    descriptor = Mock()
    descriptor.stores_state = True
    descriptor.location = location('def_id')
    descriptor.module_class.fields = fields
    descriptor.module_class.lms.fields = lms_fields
    return descriptor

location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
course_id = 'edX/test_course/test'

content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id'))
settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id'))
student_state_key = partial(LmsKeyValueStore.Key, Scope.student_state, 'user', location('def_id'))
student_prefs_key = partial(LmsKeyValueStore.Key, Scope.student_preferences, 'user', 'problem')
student_info_key = partial(LmsKeyValueStore.Key, Scope.student_info, 'user', None)


class UserFactory(factory.Factory):
    FACTORY_FOR = User

    username = 'user'


class StudentModuleFactory(factory.Factory):
    FACTORY_FOR = StudentModule

    module_type = 'problem'
    module_state_key = location('def_id').url()
    student = factory.SubFactory(UserFactory)
    course_id = course_id
    state = None


class ContentFactory(factory.Factory):
    FACTORY_FOR = XModuleContentField

    field_name = 'existing_field'
    value = json.dumps('old_value')
    definition_id = location('def_id').url()


class SettingsFactory(factory.Factory):
    FACTORY_FOR = XModuleSettingsField

    field_name = 'existing_field'
    value = json.dumps('old_value')
    usage_id = '%s-%s' % (course_id, location('def_id').url())


class StudentPrefsFactory(factory.Factory):
    FACTORY_FOR = XModuleStudentPrefsField

    field_name = 'existing_field'
    value = json.dumps('old_value')
    student = factory.SubFactory(UserFactory)
    module_type = 'problem'


class StudentInfoFactory(factory.Factory):
    FACTORY_FOR = XModuleStudentInfoField

    field_name = 'existing_field'
    value = json.dumps('old_value')
    student = factory.SubFactory(UserFactory)


class TestDescriptorFallback(TestCase):

    def setUp(self):
        self.desc_md = {
            'field_a': 'content',
            'field_b': 'settings',
        }
        self.kvs = LmsKeyValueStore(self.desc_md, None)

    def test_get_from_descriptor(self):
        self.assertEquals('content', self.kvs.get(content_key('field_a')))
        self.assertEquals('settings', self.kvs.get(settings_key('field_b')))

    def test_write_to_descriptor(self):
        self.assertRaises(InvalidWriteError, self.kvs.set, content_key('field_a'), 'foo')
        self.assertEquals('content', self.desc_md['field_a'])
        self.assertRaises(InvalidWriteError, self.kvs.set, settings_key('field_b'), 'foo')
        self.assertEquals('settings', self.desc_md['field_b'])

        self.assertRaises(InvalidWriteError, self.kvs.delete, content_key('field_a'))
        self.assertEquals('content', self.desc_md['field_a'])
        self.assertRaises(InvalidWriteError, self.kvs.delete, settings_key('field_b'))
        self.assertEquals('settings', self.desc_md['field_b'])


class TestInvalidScopes(TestCase):
    def setUp(self):
        self.desc_md = {}
        self.user = UserFactory.create()
        self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user)
        self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)

    def test_invalid_scopes(self):
        for scope in (Scope(student=True, block=BlockScope.DEFINITION),
                      Scope(student=False, block=BlockScope.TYPE),
                      Scope(student=False, block=BlockScope.ALL)):
            self.assertRaises(InvalidScopeError, self.kvs.get, LmsKeyValueStore.Key(scope, None, None, 'field'))
            self.assertRaises(InvalidScopeError, self.kvs.set, LmsKeyValueStore.Key(scope, None, None, 'field'), 'value')
            self.assertRaises(InvalidScopeError, self.kvs.delete, LmsKeyValueStore.Key(scope, None, None, 'field'))
            self.assertRaises(InvalidScopeError, self.kvs.has, LmsKeyValueStore.Key(scope, None, None, 'field'))


class TestStudentModuleStorage(TestCase):

    def setUp(self):
        self.desc_md = {}
        student_module = StudentModuleFactory(state=json.dumps({'a_field': 'a_value'}))
        self.user = student_module.student
        self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user)
        self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)

    def test_get_existing_field(self):
        "Test that getting an existing field in an existing StudentModule works"
        self.assertEquals('a_value', self.kvs.get(student_state_key('a_field')))

    def test_get_missing_field(self):
        "Test that getting a missing field from an existing StudentModule raises a KeyError"
        self.assertRaises(KeyError, self.kvs.get, student_state_key('not_a_field'))

    def test_set_existing_field(self):
        "Test that setting an existing student_state field changes the value"
        self.kvs.set(student_state_key('a_field'), 'new_value')
        self.assertEquals(1, StudentModule.objects.all().count())
        self.assertEquals({'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))

    def test_set_missing_field(self):
        "Test that setting a new student_state field changes the value"
        self.kvs.set(student_state_key('not_a_field'), 'new_value')
        self.assertEquals(1, StudentModule.objects.all().count())
        self.assertEquals({'a_field': 'a_value', 'not_a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))

    def test_delete_existing_field(self):
        "Test that deleting an existing field removes it from the StudentModule"
        self.kvs.delete(student_state_key('a_field'))
        self.assertEquals(1, StudentModule.objects.all().count())
        self.assertRaises(KeyError, self.kvs.get, student_state_key('not_a_field'))

    def test_delete_missing_field(self):
        "Test that deleting a missing field from an existing StudentModule raises a KeyError"
        self.assertRaises(KeyError, self.kvs.delete, student_state_key('not_a_field'))
        self.assertEquals(1, StudentModule.objects.all().count())
        self.assertEquals({'a_field': 'a_value'}, json.loads(StudentModule.objects.all()[0].state))

    def test_has_existing_field(self):
        "Test that `has` returns True for existing fields in StudentModules"
        self.assertTrue(self.kvs.has(student_state_key('a_field')))

    def test_has_missing_field(self):
        "Test that `has` returns False for missing fields in StudentModule"
        self.assertFalse(self.kvs.has(student_state_key('not_a_field')))


class TestMissingStudentModule(TestCase):
    def setUp(self):
        self.user = UserFactory.create()
        self.desc_md = {}
        self.mdc = ModelDataCache([mock_descriptor()], course_id, self.user)
        self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)

    def test_get_field_from_missing_student_module(self):
        "Test that getting a field from a missing StudentModule raises a KeyError"
        self.assertRaises(KeyError, self.kvs.get, student_state_key('a_field'))

    def test_set_field_in_missing_student_module(self):
        "Test that setting a field in a missing StudentModule creates the student module"
        self.assertEquals(0, len(self.mdc.cache))
        self.assertEquals(0, StudentModule.objects.all().count())

        self.kvs.set(student_state_key('a_field'), 'a_value')

        self.assertEquals(1, len(self.mdc.cache))
        self.assertEquals(1, StudentModule.objects.all().count())

        student_module = StudentModule.objects.all()[0]
        self.assertEquals({'a_field': 'a_value'}, json.loads(student_module.state))
        self.assertEquals(self.user, student_module.student)
        self.assertEquals(location('def_id').url(), student_module.module_state_key)
        self.assertEquals(course_id, student_module.course_id)

    def test_delete_field_from_missing_student_module(self):
        "Test that deleting a field from a missing StudentModule raises a KeyError"
        self.assertRaises(KeyError, self.kvs.delete, student_state_key('a_field'))

    def test_has_field_for_missing_student_module(self):
        "Test that `has` returns False for missing StudentModules"
        self.assertFalse(self.kvs.has(student_state_key('a_field')))


class StorageTestBase(object):
    factory = None
    scope = None
    key_factory = None
    storage_class = None

    def setUp(self):
        field_storage = self.factory.create()
        if hasattr(field_storage, 'student'):
            self.user = field_storage.student
        else:
            self.user = UserFactory.create()
        self.desc_md = {}
        self.mdc = ModelDataCache([mock_descriptor([mock_field(self.scope, 'existing_field')])], course_id, self.user)
        self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)

    def test_get_existing_field(self):
        "Test that getting an existing field in an existing Storage Field works"
        self.assertEquals('old_value', self.kvs.get(self.key_factory('existing_field')))

    def test_get_missing_field(self):
        "Test that getting a missing field from an existing Storage Field raises a KeyError"
        self.assertRaises(KeyError, self.kvs.get, self.key_factory('missing_field'))

    def test_set_existing_field(self):
        "Test that setting an existing field changes the value"
        self.kvs.set(self.key_factory('existing_field'), 'new_value')
        self.assertEquals(1, self.storage_class.objects.all().count())
        self.assertEquals('new_value', json.loads(self.storage_class.objects.all()[0].value))

    def test_set_missing_field(self):
        "Test that setting a new field changes the value"
        self.kvs.set(self.key_factory('missing_field'), 'new_value')
        self.assertEquals(2, self.storage_class.objects.all().count())
        self.assertEquals('old_value', json.loads(self.storage_class.objects.get(field_name='existing_field').value))
        self.assertEquals('new_value', json.loads(self.storage_class.objects.get(field_name='missing_field').value))

    def test_delete_existing_field(self):
        "Test that deleting an existing field removes it"
        self.kvs.delete(self.key_factory('existing_field'))
        self.assertEquals(0, self.storage_class.objects.all().count())

    def test_delete_missing_field(self):
        "Test that deleting a missing field from an existing Storage Field raises a KeyError"
        self.assertRaises(KeyError, self.kvs.delete, self.key_factory('missing_field'))
        self.assertEquals(1, self.storage_class.objects.all().count())

    def test_has_existing_field(self):
        "Test that `has` returns True for an existing Storage Field"
        self.assertTrue(self.kvs.has(self.key_factory('existing_field')))

    def test_has_missing_field(self):
        "Test that `has` return False for an existing Storage Field"
        self.assertFalse(self.kvs.has(self.key_factory('missing_field')))


class TestSettingsStorage(StorageTestBase, TestCase):
    factory = SettingsFactory
    scope = Scope.settings
    key_factory = settings_key
    storage_class = XModuleSettingsField


class TestContentStorage(StorageTestBase, TestCase):
    factory = ContentFactory
    scope = Scope.content
    key_factory = content_key
    storage_class = XModuleContentField


class TestStudentPrefsStorage(StorageTestBase, TestCase):
    factory = StudentPrefsFactory
    scope = Scope.student_preferences
    key_factory = student_prefs_key
    storage_class = XModuleStudentPrefsField


class TestStudentInfoStorage(StorageTestBase, TestCase):
    factory = StudentInfoFactory
    scope = Scope.student_info
    key_factory = student_info_key
    storage_class = XModuleStudentInfoField
