"""
Test helpers for testing course block transformers.
"""
from mock import patch

from course_modes.models import CourseMode
from lms.djangoapps.courseware.access import has_access
from openedx.core.djangoapps.content.block_structure.tests.helpers import clear_registered_transformers_cache
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory

from ...api import get_course_blocks


class TransformerRegistryTestMixin(object):
    """
    Mixin that overrides the TransformerRegistry so that it returns
    TRANSFORMER_CLASS_TO_TEST as a registered transformer.
    """
    def setUp(self):
        super(TransformerRegistryTestMixin, self).setUp()
        self.patcher = patch(
            'openedx.core.djangoapps.content.block_structure.transformer_registry.'
            'TransformerRegistry.get_registered_transformers'
        )
        mock_registry = self.patcher.start()
        mock_registry.return_value = {self.TRANSFORMER_CLASS_TO_TEST}
        self.transformers = BlockStructureTransformers([self.TRANSFORMER_CLASS_TO_TEST()])

    def tearDown(self):
        self.patcher.stop()
        clear_registered_transformers_cache()


class CourseStructureTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase):
    """
    Helper for test cases that need to build course structures.
    """
    def setUp(self):
        """
        Create users.
        """
        super(CourseStructureTestCase, self).setUp()
        # Set up users.
        self.password = 'test'
        self.user = UserFactory.create(password=self.password)
        self.staff = UserFactory.create(password=self.password, is_staff=True)

    def create_block_id(self, block_type, block_ref):
        """
        Returns the block id (display name) that is used in the test
        course structures for the given block type and block reference
        string.
        """
        return '{}_{}'.format(block_type, block_ref)

    def build_xblock(self, block_hierarchy, block_map, parent):
        """
        Build an XBlock, add it to block_map, and call build_xblock on
        the children defined in block_dict.

        Arguments:
            block_hierarchy (BlockStructureDict): Definition of
                hierarchy, from this block down.
            block_map (dict[str: XBlock]): Mapping from '#ref' values to
                their XBlocks.
            parent (XBlock): Parent block for this xBlock.
        """
        block_type = block_hierarchy['#type']
        block_ref = block_hierarchy['#ref']
        factory = (CourseFactory if block_type == 'course' else ItemFactory)
        kwargs = {key: value for key, value in block_hierarchy.iteritems() if key[0] != '#'}

        if block_type != 'course':
            kwargs['category'] = block_type
            kwargs['publish_item'] = True,
        if parent:
            kwargs['parent'] = parent

        xblock = factory.create(
            display_name=self.create_block_id(block_type, block_ref),
            **kwargs
        )
        block_map[block_ref] = xblock

        for child_hierarchy in block_hierarchy.get('#children', []):
            self.build_xblock(child_hierarchy, block_map, xblock)

    def add_parents(self, block_hierarchy, block_map):
        """
        Recursively traverse the block_hierarchy and add additional
        parents. This method is expected to be called only after all
        blocks have been created.

        The additional parents are obtained from the '#parents' field
        and is expected to be a list of '#ref' values of the parents.

        Note: if a '#parents' field is found, the block is removed from
        the course block since it is expected to not belong to the root.
        If the block is meant to be a direct child of the course as
        well, the course should be explicitly listed in '#parents'.

        Arguments:
            block_hierarchy (BlockStructureDict):
                Definition of block hierarchy.
            block_map (dict[str: XBlock]):
                Mapping from '#ref' values to their XBlocks.

        """
        parents = block_hierarchy.get('#parents', [])
        if parents:
            block_key = block_map[block_hierarchy['#ref']].location

            # First remove the block from the course.
            # It would be re-added to the course if the course was
            # explicitly listed in parents.
            course = modulestore().get_item(block_map['course'].location)
            if block_key in course.children:
                course.children.remove(block_key)
                block_map['course'] = update_block(course)

            # Add this to block to each listed parent.
            for parent_ref in parents:
                parent_block = modulestore().get_item(block_map[parent_ref].location)
                parent_block.children.append(block_key)
                block_map[parent_ref] = update_block(parent_block)

        # recursively call the children
        for child_hierarchy in block_hierarchy.get('#children', []):
            self.add_parents(child_hierarchy, block_map)

    def build_course(self, course_hierarchy):
        """
        Build a hierarchy of XBlocks.

        Arguments:
            course_hierarchy (BlockStructureDict): Definition of course
                hierarchy.

            where a BlockStructureDict is a list of dicts in the form {
                'key1': 'value1',
                ...
                'keyN': 'valueN',
                '#type': block_type,
                '#ref': short_string_for_referencing_block,
                '#children': list[BlockStructureDict],
                '#parents': list['#ref' values]
            }

            Special keys start with '#'; the rest just get passed as
            kwargs to Factory.create.

            Note: the caller has a choice of whether to create
            (1) a nested block structure with children blocks embedded
                within their parents, or
            (2) a flat block structure with children blocks defined
                alongside their parents and attached via the #parents
                field, or
            (3) a combination of both #1 and #2 used for whichever
                blocks.

            Note 2: When the #parents field is used in addition to the
            nested pattern for a block, it specifies additional parents
            that aren't already implied by having the block exist within
            another block's #children field.

        Returns:
            dict[str: XBlock]:
                Mapping from '#ref' values to their XBlocks.
        """
        block_map = {}

        # build the course tree
        for block_hierarchy in course_hierarchy:
            self.build_xblock(block_hierarchy, block_map, parent=None)

        # add additional parents if the course is a DAG or built
        # linearly (without specifying '#children' values)
        for block_hierarchy in course_hierarchy:
            self.add_parents(block_hierarchy, block_map)

        publish_course(block_map['course'])

        return block_map

    def get_block_key_set(self, blocks, *refs):
        """
        Gets the set of usage keys that correspond to the list of
        #ref values as defined on blocks.

        Returns: set[UsageKey]
        """
        xblocks = (blocks[ref] for ref in refs)
        return set([xblock.location for xblock in xblocks])


class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase):
    """
    Test helper class for creating a test course of
    a graph of vertical blocks based on a parents_map.
    """

    # Tree formed by parent_map:
    #        0
    #     /     \
    #    1       2
    #   / \     / \
    #  3   4   /   5
    #       \ /
    #        6
    # Note the parents must always have lower indices than their
    # children.
    parents_map = [[], [0], [0], [1], [1], [2], [2, 4]]

    def setUp(self, **kwargs):
        super(BlockParentsMapTestCase, self).setUp(**kwargs)

        # create the course
        self.course = CourseFactory.create()

        # an ordered list of block locations, where the index
        # corresponds to the block's index in the parents_map.
        self.xblock_keys = [self.course.location]

        # create all other blocks in the course
        for i, parents_index in enumerate(self.parents_map):
            if i == 0:
                continue  # course already created

            self.xblock_keys.append(
                ItemFactory.create(
                    parent=self.get_block(parents_index[0]),
                    category="vertical",
                ).location
            )

            # add additional parents
            if len(parents_index) > 1:
                for index in range(1, len(parents_index)):
                    parent_index = parents_index[index]
                    parent_block = self.get_block(parent_index)
                    parent_block.children.append(self.xblock_keys[i])
                    update_block(parent_block)

        self.password = 'test'
        self.student = UserFactory.create(is_staff=False, username='test_student', password=self.password)
        self.staff = UserFactory.create(is_staff=True, username='test_staff', password=self.password)
        CourseEnrollmentFactory.create(
            is_active=True,
            mode=CourseMode.DEFAULT_MODE_SLUG,
            user=self.student,
            course_id=self.course.id
        )

    def assert_transform_results(
            self,
            test_user,
            expected_user_accessible_blocks,
            blocks_with_differing_access=None,
            transformers=None,
    ):
        """
        Verifies the results of transforming the blocks in the course.

        Arguments:
            test_user (User): The non-staff user that is being tested.
                For example, self.student.

            expected_user_accessible_blocks (set(int)): Set of blocks
                (indices) that a student user is expected to have access
                to after the transformers are executed.

            blocks_with_differing_access (set(int)): Set of
                blocks (indices) whose access will differ from the
                transformers result and the current implementation of
                has_access.  If not provided, does not compare with
                has_access results.

            transformers (BlockStructureTransformers): An optional collection
                of transformers that are to be executed.  If not
                provided, the default value used by get_course_blocks
                is used.
        """
        publish_course(self.course)

        # verify given test user has access to expected blocks
        self._check_results(
            test_user,
            expected_user_accessible_blocks,
            blocks_with_differing_access,
            transformers,
        )

        # verify staff has access to all blocks
        self._check_results(self.staff, set(range(len(self.parents_map))), {}, transformers)

    def get_block(self, block_index):
        """
        Helper method to retrieve the requested block (index) from the
        modulestore
        """
        return modulestore().get_item(self.xblock_keys[block_index])

    def _check_results(self, user, expected_accessible_blocks, blocks_with_differing_access, transformers):
        """
        Verifies the results of transforming the blocks in the
        course for the given user.
        """

        self.client.login(username=user.username, password=self.password)
        block_structure = get_course_blocks(user, self.course.location, transformers)

        for i, xblock_key in enumerate(self.xblock_keys):

            # compute access results of the block
            block_structure_result = xblock_key in block_structure

            # compare with expected value
            self.assertEquals(
                block_structure_result,
                i in expected_accessible_blocks,
                "block_structure return value {0} not equal to expected value for block {1} for user {2}".format(
                    block_structure_result, i, user.username
                )
            )

            if blocks_with_differing_access:
                # compare with has_access_result
                has_access_result = bool(has_access(user, 'load', self.get_block(i), course_key=self.course.id))
                if i in blocks_with_differing_access:
                    self.assertNotEqual(
                        block_structure_result,
                        has_access_result,
                        "block structure ({0}) & has_access ({1}) results are equal for block {2} for user {3}".format(
                            block_structure_result, has_access_result, i, user.username
                        )
                    )
                else:
                    self.assertEquals(
                        block_structure_result,
                        has_access_result,
                        "block structure ({0}) & has_access ({1}) results not equal for block {2} for user {3}".format(
                            block_structure_result, has_access_result, i, user.username
                        )
                    )

        self.client.logout()


def update_block(block):
    """
    Helper method to update the block in the modulestore
    """
    return modulestore().update_item(block, ModuleStoreEnum.UserID.test)


def publish_course(course):
    """
    Helper method to publish the course (from draft to publish branch)
    """
    store = modulestore()
    with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course.id):
        store.publish(course.location, ModuleStoreEnum.UserID.test)


def create_location(org, course, run, block_type, block_id):
    """
    Returns the usage key for the given key parameters using the
    default modulestore
    """
    return modulestore().make_course_key(org, course, run).make_usage_key(block_type, block_id)
