from django.test import TestCase, RequestFactory
from graphene.test import Client
from graphql_relay import to_global_id

from api.schema import schema
from books.factories import ModuleFactory
from books.models import ContentBlock, Chapter
from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory
from users.factories import SchoolClassFactory
from users.models import User
from users.services import create_users

TEMPLATE_CLASS_NAME = 'template-class'
SYNC_CLASS_NAME = 'class-to-be-synced'

SCHOOL_CLASS_FRAGMENT = """
fragment SchoolClassFragment on SchoolClassNode {
  id
  name
}
"""

CONTENT_BLOCK_QUERY = SCHOOL_CLASS_FRAGMENT + """
query ContentBlockQuery($id: ID!) {
  contentBlock(id: $id) {
    hiddenFor {
      ...SchoolClassFragment
    }
    visibleFor {
      ...SchoolClassFragment
    }
  }
}
"""

CHAPTER_QUERY = SCHOOL_CLASS_FRAGMENT + """
query ChapterQuery($id: ID!) {
    chapter(id: $id) {
        id
        titleHiddenFor {
          ...SchoolClassFragment
        }
        descriptionHiddenFor {
          ...SchoolClassFragment
        }
    }    
}
"""

OBJECTIVE_GROUP_QUERY = SCHOOL_CLASS_FRAGMENT + """
query ObjectiveGroupQuery($id: ID!) {
  objectiveGroup(id: $id) {
    hiddenFor {
      ...SchoolClassFragment
    }
    objectives {
      id
      text
      hiddenFor {
        ...SchoolClassFragment
      }
      visibleFor {
        ...SchoolClassFragment
      }
    }
  }
}

"""

SYNC_MUTATION = """
mutation SyncMutationVisibility($input: SyncModuleVisibilityInput!) {
  syncModuleVisibility(input: $input) {
    success
  }
}
"""


class CopyVisibilityForClassesTestCase(TestCase):
    """
     what do we want to happen?
     we have 3 public content blocks                                                                [X, Y, Z]
     we have 2 custom content block                                                                 [M, N]
     we have 2 school classes                                                                       [A, B]
     one public content block is hidden for class A                                                 | [X, Y]
     one custom content block is visible for class A                                                | [M]
     class B also sees two of three public content blocks, but one is different from what A sees    | [X, Z]
     class B doesn't see the custom content block, but another one                                  | [N]
     so A sees                                                                                      | [X, Y, M]
     B sees                                                                                         | [X, Z, N]
     we want to copy the settings from class A to class B
     now class B sees the same content blocks as class A                                            | [X, Y, N]
     """

    def setUp(self):
        # create users and school classes
        create_users()
        teacher = User.objects.get(username='teacher')
        student1 = User.objects.get(username='student1')
        student2 = User.objects.get(username='student2')
        # school class to be used as the pattern or model
        template_school_class = SchoolClassFactory(name=TEMPLATE_CLASS_NAME, users=[teacher, student1])
        # school class to be synced, e.g. adapted to be like the other
        school_class_to_be_synced = SchoolClassFactory(name=SYNC_CLASS_NAME, users=[teacher, student2])

        # create content
        module = ModuleFactory(slug='some-module')
        chapter = Chapter(title='Some Chapter')
        module.add_child(instance=chapter)
        default_content_block = ContentBlock(title='default block', slug='default')
        hidden_content_block = ContentBlock(title='hidden block', slug='hidden')
        other_hidden_content_block = ContentBlock(title='other hidden block', slug='other-hidden')
        custom_content_block = ContentBlock(title='custom block', slug='custom', owner=teacher)
        other_custom_content_block = ContentBlock(title='other custom block', slug='other-custom', owner=teacher)

        objective_group = ObjectiveGroupFactory(module=module)
        objective = ObjectiveFactory(group=objective_group)

        chapter.specific.add_child(instance=default_content_block)
        chapter.specific.add_child(instance=hidden_content_block)
        chapter.specific.add_child(instance=custom_content_block)
        chapter.specific.add_child(instance=other_custom_content_block)
        chapter.specific.add_child(instance=other_hidden_content_block)

        hidden_content_block.hidden_for.add(template_school_class)
        custom_content_block.visible_for.add(template_school_class)

        other_hidden_content_block.hidden_for.add(school_class_to_be_synced)
        other_custom_content_block.visible_for.add(school_class_to_be_synced)

        # hide chapter title and description for student 1 in template-school-class
        chapter.title_hidden_for.add(template_school_class)
        chapter.description_hidden_for.add(template_school_class)

        # hide objectives for template-school-class
        objective_group.hidden_for.add(template_school_class)
        objective.hidden_for.add(template_school_class)

        teacher_request = RequestFactory().get('/')
        teacher_request.user = teacher
        self.teacher_client = Client(schema=schema, context_value=teacher_request)

        student1_request = RequestFactory().get('/')
        student1_request.user = student1
        self.student1_client = Client(schema=schema, context_value=student1_request)

        student2_request = RequestFactory().get('/')
        student2_request.user = student2
        self.student2_client = Client(schema=schema, context_value=student2_request)

        # set references
        self.template_school_class = template_school_class
        self.school_class_to_be_synced = school_class_to_be_synced
        self.module = module
        self.chapter = to_global_id('ChapterNode', chapter.pk)
        self.objective_group_id = to_global_id('ObjectiveGroupNode', objective_group.pk)
        self.default_content_block = to_global_id('ContentBlockNode', default_content_block.pk)
        self.hidden_content_block = to_global_id('ContentBlockNode', hidden_content_block.pk)
        self.custom_content_block = to_global_id('ContentBlockNode', custom_content_block.pk)
        self.other_custom_content_block = to_global_id('ContentBlockNode', other_custom_content_block.pk)
        self.other_hidden_content_block = to_global_id('ContentBlockNode', other_hidden_content_block.pk)

    def _get_result(self, query, client, id):
        result = client.execute(query, variables={
            'id': id
        })
        return result

    def _execute_sync(self):
        self.teacher_client.execute(SYNC_MUTATION, variables={
            'input': {
                'module': self.module.slug,
                'templateSchoolClass': to_global_id('SchoolClassNode', self.template_school_class.pk),
                'schoolClass': to_global_id('SchoolClassNode', self.school_class_to_be_synced.pk)
            }
        })

    def _test_in_sync(self):
        # the hidden block is hidden for both now
        hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.hidden_content_block)
        hidden_for = hidden_result.get('data').get('contentBlock').get('hiddenFor')
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], hidden_for))
        self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['name'], hidden_for))

        # the other hidden block is hidden for no one now
        other_hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
                                               self.other_hidden_content_block)
        hidden_for = other_hidden_result.get('data').get('contentBlock').get('hiddenFor')
        self.assertEqual(len(hidden_for), 0)

        # the default block is still hidden for no one
        default_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.default_content_block)
        hidden_for = default_result.get('data').get('contentBlock').get('hiddenFor')
        self.assertEqual(len(hidden_for), 0)

        # the custom block is visible for both
        custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.custom_content_block)
        visible_for = custom_result.get('data').get('contentBlock').get('visibleFor')
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], visible_for))
        self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['name'], visible_for))

        # the other custom block is visible for no one
        other_custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
                                               self.other_custom_content_block)
        visible_for = other_custom_result.get('data').get('contentBlock').get('visibleFor')
        self.assertEqual(len(visible_for), 0)

    def test_hidden_for_and_visible_for_set_correctly(self):
        self.assertEqual(ContentBlock.objects.count(), 5)

        hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.hidden_content_block)
        hidden_for = hidden_result.get('data').get('contentBlock').get('hiddenFor')
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], hidden_for))
        self.assertFalse(SYNC_CLASS_NAME in map(lambda x: x['name'], hidden_for))

        other_hidden_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
                                               self.other_hidden_content_block)
        hidden_for = other_hidden_result.get('data').get('contentBlock').get('hiddenFor')
        self.assertFalse(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], hidden_for))
        self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['name'], hidden_for))

        default_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.default_content_block)
        hidden_for = default_result.get('data').get('contentBlock').get('hiddenFor')
        self.assertEqual(len(hidden_for), 0)

        custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client, self.custom_content_block)
        visible_for = custom_result.get('data').get('contentBlock').get('visibleFor')
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], visible_for))
        self.assertFalse(SYNC_CLASS_NAME in map(lambda x: x['name'], visible_for))

        other_custom_result = self._get_result(CONTENT_BLOCK_QUERY, self.teacher_client,
                                               self.other_custom_content_block)
        visible_for = other_custom_result.get('data').get('contentBlock').get('visibleFor')
        self.assertFalse(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], visible_for))
        self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['name'], visible_for))

    def test_syncs_correctly(self):
        self.module.sync_from_school_class(self.template_school_class, self.school_class_to_be_synced)

        self._test_in_sync()

    def test_mutation(self):
        self._execute_sync()
        self._test_in_sync()

    def test_chapter_visibility(self):
        # chapter title and description hidden for class A
        # chapter title and description visible for class B
        # sync module
        # chapter title and description hidden for class B
        query = CHAPTER_QUERY
        variables = {"id": self.chapter}

        result = self.student1_client.execute(query, variables=variables)
        self.assertIsNone(result.get('errors'))
        chapter = result.get('data').get('chapter')
        title_hidden_for = chapter.get('titleHiddenFor')
        description_hidden_for = chapter.get('descriptionHiddenFor')
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], title_hidden_for))
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], description_hidden_for))
        self.assertTrue(SYNC_CLASS_NAME not in map(lambda x: x['name'], title_hidden_for))
        self.assertTrue(SYNC_CLASS_NAME not in map(lambda x: x['name'], description_hidden_for))

        self._execute_sync()

        result = self.student1_client.execute(query, variables=variables)
        self.assertIsNone(result.get('errors'))
        chapter = result.get('data').get('chapter')
        title_hidden_for = chapter.get('titleHiddenFor')
        description_hidden_for = chapter.get('descriptionHiddenFor')
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], title_hidden_for))
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], description_hidden_for))
        self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['name'], title_hidden_for))
        self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['name'], description_hidden_for))

    def _objective_group_query(self):
        query = OBJECTIVE_GROUP_QUERY
        variables = {"id": self.objective_group_id}
        return query, variables

    def _get_objective_group(self, client, query, variables):
        result = client.execute(query, variables=variables)
        self.assertIsNone(result.get('errors'))

        objective_group = result.get('data').get('objectiveGroup')
        return objective_group

    def test_objective_group_visibility(self):
        query, variables = self._objective_group_query()
        objective_group = self._get_objective_group(self.student1_client, query, variables)
        hidden_for = objective_group.get('hiddenFor')
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], hidden_for))
        self.assertTrue(SYNC_CLASS_NAME not in map(lambda x: x['name'], hidden_for))

        self._execute_sync()

        objective_group = self._get_objective_group(self.student1_client, query, variables)
        hidden_for = objective_group.get('hiddenFor')
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], hidden_for))
        self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['name'], hidden_for))

    def test_objective_visibility(self):
        query, variables = self._objective_group_query()
        objective_group = self._get_objective_group(self.student2_client, query, variables)
        objective = objective_group.get('objectives')[0]
        hidden_for = objective.get('hiddenFor')
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], hidden_for))
        self.assertTrue(SYNC_CLASS_NAME not in map(lambda x: x['name'], hidden_for))

        self._execute_sync()

        objective_group = self._get_objective_group(self.student2_client, query, variables)
        objective = objective_group.get('objectives')[0]
        hidden_for = objective.get('hiddenFor')
        self.assertTrue(TEMPLATE_CLASS_NAME in map(lambda x: x['name'], hidden_for))
        self.assertTrue(SYNC_CLASS_NAME in map(lambda x: x['name'], hidden_for))
