"""
This module defines tests for courseware.access that are specific to group
access control rules.
"""

import ddt
from nose.plugins.attrib import attr
from stevedore.extension import Extension, ExtensionManager

from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.partitions.partitions import Group, UserPartition, USER_PARTITION_SCHEME_NAMESPACE
from xmodule.modulestore.django import modulestore

import courseware.access as access
from courseware.tests.factories import StaffFactory, UserFactory


class MemoryUserPartitionScheme(object):
    """
    In-memory partition scheme for testing.
    """
    name = "memory"

    def __init__(self):
        self.current_group = {}

    def set_group_for_user(self, user, user_partition, group):
        """
        Link this user to this group in this partition, in memory.
        """
        self.current_group.setdefault(user.id, {})[user_partition.id] = group

    def get_group_for_user(self, course_id, user, user_partition, track_function=None):  # pylint: disable=unused-argument
        """
        Fetch the group to which this user is linked in this partition, or None.
        """
        return self.current_group.get(user.id, {}).get(user_partition.id)


def resolve_attrs(test_method):
    """
    Helper function used with ddt.  It allows passing strings to test methods
    via @ddt.data, which are the names of instance attributes on `self`, and
    replaces them with the resolved values of those attributes in the method
    call.
    """
    def _wrapper(self, *args):  # pylint: disable=missing-docstring
        new_args = [getattr(self, arg) for arg in args]
        return test_method(self, *new_args)
    return _wrapper


@attr('shard_1')
@ddt.ddt
class GroupAccessTestCase(ModuleStoreTestCase):
    """
    Tests to ensure that has_access() correctly enforces the visibility
    restrictions specified in the `group_access` field of XBlocks.
    """
    def set_user_group(self, user, partition, group):
        """
        Internal DRY / shorthand.
        """
        partition.scheme.set_group_for_user(user, partition, group)

    def set_group_access(self, block_location, access_dict):
        """
        Set group_access on block specified by location.
        """
        block = modulestore().get_item(block_location)
        block.group_access = access_dict
        modulestore().update_item(block, 1)

    def set_user_partitions(self, block_location, partitions):
        """
        Sets the user_partitions on block specified by location.
        """
        block = modulestore().get_item(block_location)
        block.user_partitions = partitions
        modulestore().update_item(block, 1)

    def setUp(self):
        super(GroupAccessTestCase, self).setUp()

        UserPartition.scheme_extensions = ExtensionManager.make_test_instance(
            [
                Extension(
                    "memory",
                    USER_PARTITION_SCHEME_NAMESPACE,
                    MemoryUserPartitionScheme(),
                    None
                ),
                Extension(
                    "random",
                    USER_PARTITION_SCHEME_NAMESPACE,
                    MemoryUserPartitionScheme(),
                    None
                )
            ],
            namespace=USER_PARTITION_SCHEME_NAMESPACE
        )

        self.cat_group = Group(10, 'cats')
        self.dog_group = Group(20, 'dogs')
        self.worm_group = Group(30, 'worms')
        self.animal_partition = UserPartition(
            0,
            'Pet Partition',
            'which animal are you?',
            [self.cat_group, self.dog_group, self.worm_group],
            scheme=UserPartition.get_scheme("memory"),
        )

        self.red_group = Group(1000, 'red')
        self.blue_group = Group(2000, 'blue')
        self.gray_group = Group(3000, 'gray')
        self.color_partition = UserPartition(
            100,
            'Color Partition',
            'what color are you?',
            [self.red_group, self.blue_group, self.gray_group],
            scheme=UserPartition.get_scheme("memory"),
        )

        self.course = CourseFactory.create(
            user_partitions=[self.animal_partition, self.color_partition],
        )
        with self.store.bulk_operations(self.course.id, emit_signals=False):
            chapter = ItemFactory.create(category='chapter', parent=self.course)
            section = ItemFactory.create(category='sequential', parent=chapter)
            vertical = ItemFactory.create(category='vertical', parent=section)
            component = ItemFactory.create(category='problem', parent=vertical)

            self.chapter_location = chapter.location
            self.section_location = section.location
            self.vertical_location = vertical.location
            self.component_location = component.location

        self.red_cat = UserFactory()  # student in red and cat groups
        self.set_user_group(self.red_cat, self.animal_partition, self.cat_group)
        self.set_user_group(self.red_cat, self.color_partition, self.red_group)

        self.blue_dog = UserFactory()  # student in blue and dog groups
        self.set_user_group(self.blue_dog, self.animal_partition, self.dog_group)
        self.set_user_group(self.blue_dog, self.color_partition, self.blue_group)

        self.white_mouse = UserFactory()  # student in no group

        self.gray_worm = UserFactory()  # student in deleted group
        self.set_user_group(self.gray_worm, self.animal_partition, self.worm_group)
        self.set_user_group(self.gray_worm, self.color_partition, self.gray_group)
        # delete the gray/worm groups from the partitions now so we can test scenarios
        # for user whose group is missing.
        self.animal_partition.groups.pop()
        self.color_partition.groups.pop()

        # add a staff user, whose access will be unconditional in spite of group access.
        self.staff = StaffFactory.create(course_key=self.course.id)

    # avoid repeatedly declaring the same sequence for ddt in all the test cases.
    PARENT_CHILD_PAIRS = (
        ('chapter_location', 'chapter_location'),
        ('chapter_location', 'section_location'),
        ('chapter_location', 'vertical_location'),
        ('chapter_location', 'component_location'),
        ('section_location', 'section_location'),
        ('section_location', 'vertical_location'),
        ('section_location', 'component_location'),
        ('vertical_location', 'vertical_location'),
        ('vertical_location', 'component_location'),
    )

    def tearDown(self):
        """
        Clear out the stevedore extension points on UserPartition to avoid
        side-effects in other tests.
        """
        UserPartition.scheme_extensions = None
        super(GroupAccessTestCase, self).tearDown()

    def check_access(self, user, block_location, is_accessible):
        """
        DRY helper.
        """
        self.assertIs(
            bool(access.has_access(user, 'load', modulestore().get_item(block_location), self.course.id)),
            is_accessible
        )

    def ensure_staff_access(self, block_location):
        """
        Another DRY helper.
        """
        block = modulestore().get_item(block_location)
        self.assertTrue(access.has_access(self.staff, 'load', block, self.course.id))

    # NOTE: in all the tests that follow, `block_specified` and
    # `block_accessed` designate the place where group_access rules are
    # specified, and where access is being checked in the test, respectively.

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_has_access_single_partition_single_group(self, block_specified, block_accessed):
        """
        Access checks are correctly enforced on the block when a single group
        is specified for a single partition.
        """
        self.set_group_access(
            block_specified,
            {self.animal_partition.id: [self.cat_group.id]},
        )
        self.check_access(self.red_cat, block_accessed, True)
        self.check_access(self.blue_dog, block_accessed, False)
        self.check_access(self.white_mouse, block_accessed, False)
        self.check_access(self.gray_worm, block_accessed, False)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_has_access_single_partition_two_groups(self, block_specified, block_accessed):
        """
        Access checks are correctly enforced on the block when multiple groups
        are specified for a single partition.
        """
        self.set_group_access(
            block_specified,
            {self.animal_partition.id: [self.cat_group.id, self.dog_group.id]},
        )
        self.check_access(self.red_cat, block_accessed, True)
        self.check_access(self.blue_dog, block_accessed, True)
        self.check_access(self.white_mouse, block_accessed, False)
        self.check_access(self.gray_worm, block_accessed, False)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_has_access_single_partition_disjoint_groups(self, block_specified, block_accessed):
        """
        When the parent's and child's group specifications do not intersect,
        access is denied to the child regardless of the user's groups.
        """
        if block_specified == block_accessed:
            # this test isn't valid unless block_accessed is a descendant of
            # block_specified.
            return

        self.set_group_access(
            block_specified,
            {self.animal_partition.id: [self.dog_group.id]},
        )
        self.set_group_access(
            block_accessed,
            {self.animal_partition.id: [self.cat_group.id]},
        )
        self.check_access(self.red_cat, block_accessed, False)
        self.check_access(self.blue_dog, block_accessed, False)
        self.check_access(self.white_mouse, block_accessed, False)
        self.check_access(self.gray_worm, block_accessed, False)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_has_access_single_empty_partition(self, block_specified, block_accessed):
        """
        No group access checks are enforced on the block when group_access
        declares a partition but does not specify any groups.
        """
        self.set_group_access(block_specified, {self.animal_partition.id: []})
        self.check_access(self.red_cat, block_accessed, True)
        self.check_access(self.blue_dog, block_accessed, True)
        self.check_access(self.white_mouse, block_accessed, True)
        self.check_access(self.gray_worm, block_accessed, True)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_has_access_empty_dict(self, block_specified, block_accessed):
        """
        No group access checks are enforced on the block when group_access is an
        empty dictionary.
        """
        self.set_group_access(block_specified, {})
        self.check_access(self.red_cat, block_accessed, True)
        self.check_access(self.blue_dog, block_accessed, True)
        self.check_access(self.white_mouse, block_accessed, True)
        self.check_access(self.gray_worm, block_accessed, True)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_has_access_none(self, block_specified, block_accessed):
        """
        No group access checks are enforced on the block when group_access is None.
        """
        self.set_group_access(block_specified, None)
        self.check_access(self.red_cat, block_accessed, True)
        self.check_access(self.blue_dog, block_accessed, True)
        self.check_access(self.white_mouse, block_accessed, True)
        self.check_access(self.gray_worm, block_accessed, True)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_has_access_single_partition_group_none(self, block_specified, block_accessed):
        """
        No group access checks are enforced on the block when group_access
        specifies a partition but its value is None.
        """
        self.set_group_access(block_specified, {self.animal_partition.id: None})
        self.check_access(self.red_cat, block_accessed, True)
        self.check_access(self.blue_dog, block_accessed, True)
        self.check_access(self.white_mouse, block_accessed, True)
        self.check_access(self.gray_worm, block_accessed, True)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_has_access_single_partition_group_empty_list(self, block_specified, block_accessed):
        """
        No group access checks are enforced on the block when group_access
        specifies a partition but its value is an empty list.
        """
        self.set_group_access(block_specified, {self.animal_partition.id: []})
        self.check_access(self.red_cat, block_accessed, True)
        self.check_access(self.blue_dog, block_accessed, True)
        self.check_access(self.white_mouse, block_accessed, True)
        self.check_access(self.gray_worm, block_accessed, True)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_has_access_nonexistent_nonempty_partition(self, block_specified, block_accessed):
        """
        Access will be denied to the block when group_access specifies a
        nonempty partition that does not exist in course.user_partitions.
        """
        self.set_group_access(block_specified, {9: [99]})
        self.check_access(self.red_cat, block_accessed, False)
        self.check_access(self.blue_dog, block_accessed, False)
        self.check_access(self.white_mouse, block_accessed, False)
        self.check_access(self.gray_worm, block_accessed, False)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_has_access_nonexistent_group(self, block_specified, block_accessed):
        """
        Access will be denied to the block when group_access contains a group
        id that does not exist in its referenced partition.
        """
        self.set_group_access(block_specified, {self.animal_partition.id: [99]})
        self.check_access(self.red_cat, block_accessed, False)
        self.check_access(self.blue_dog, block_accessed, False)
        self.check_access(self.white_mouse, block_accessed, False)
        self.check_access(self.gray_worm, block_accessed, False)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_multiple_partitions(self, block_specified, block_accessed):
        """
        Group access restrictions are correctly enforced when multiple partition
        / group rules are defined.
        """
        self.set_group_access(
            block_specified,
            {
                self.animal_partition.id: [self.cat_group.id],
                self.color_partition.id: [self.red_group.id],
            },
        )
        self.check_access(self.red_cat, block_accessed, True)
        self.check_access(self.blue_dog, block_accessed, False)
        self.check_access(self.white_mouse, block_accessed, False)
        self.check_access(self.gray_worm, block_accessed, False)
        self.ensure_staff_access(block_accessed)

    @ddt.data(*PARENT_CHILD_PAIRS)
    @ddt.unpack
    @resolve_attrs
    def test_multiple_partitions_deny_access(self, block_specified, block_accessed):
        """
        Group access restrictions correctly deny access even when some (but not
        all) group_access rules are satisfied.
        """
        self.set_group_access(
            block_specified,
            {
                self.animal_partition.id: [self.cat_group.id],
                self.color_partition.id: [self.blue_group.id],
            },
        )
        self.check_access(self.red_cat, block_accessed, False)
        self.check_access(self.blue_dog, block_accessed, False)
        self.check_access(self.gray_worm, block_accessed, False)
        self.ensure_staff_access(block_accessed)

    def test_group_access_short_circuits(self):
        """
        Test that the group_access check short-circuits if there are no user_partitions defined
        except user_partitions in use by the split_test module.
        """
        # Initially, "red_cat" user can't view the vertical.
        self.set_group_access(self.chapter_location, {self.animal_partition.id: [self.dog_group.id]})
        self.check_access(self.red_cat, self.vertical_location, False)

        # Change the vertical's user_partitions value to the empty list. Now red_cat can view the vertical.
        self.set_user_partitions(self.vertical_location, [])
        self.check_access(self.red_cat, self.vertical_location, True)

        # Change the vertical's user_partitions value to include only "split_test" partitions.
        split_test_partition = UserPartition(
            199,
            'split_test partition',
            'nothing to look at here',
            [Group(2, 'random group')],
            scheme=UserPartition.get_scheme("random"),
        )
        self.set_user_partitions(self.vertical_location, [split_test_partition])
        self.check_access(self.red_cat, self.vertical_location, True)

        # Finally, add back in a cohort user_partition
        self.set_user_partitions(self.vertical_location, [split_test_partition, self.animal_partition])
        self.check_access(self.red_cat, self.vertical_location, False)
