"""
Tests for the Split Testing Module
"""
import ddt
import lxml
from mock import Mock, patch
from fs.memoryfs import MemoryFS

from xmodule.tests.xml import factories as xml
from xmodule.tests.xml import XModuleXmlImportTest
from xmodule.tests import get_test_system
from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW
from xmodule.validation import StudioValidationMessage
from xmodule.split_test_module import SplitTestDescriptor, SplitTestFields
from xmodule.partitions.partitions import Group, UserPartition
from xmodule.partitions.test_partitions import StaticPartitionService, MemoryUserTagsService


class SplitTestModuleFactory(xml.XmlImportFactory):
    """
    Factory for generating SplitTestModules for testing purposes
    """
    tag = 'split_test'


class SplitTestModuleTest(XModuleXmlImportTest):
    """
    Base class for all split_module tests.
    """
    def setUp(self):
        self.course_id = 'test_org/test_course_number/test_run'
        # construct module
        course = xml.CourseFactory.build()
        sequence = xml.SequenceFactory.build(parent=course)
        split_test = SplitTestModuleFactory(
            parent=sequence,
            attribs={
                'user_partition_id': '0',
                'group_id_to_child': '{"0": "i4x://edX/xml_test_course/html/split_test_cond0", "1": "i4x://edX/xml_test_course/html/split_test_cond1"}'
            }
        )
        xml.HtmlFactory(parent=split_test, url_name='split_test_cond0', text='HTML FOR GROUP 0')
        xml.HtmlFactory(parent=split_test, url_name='split_test_cond1', text='HTML FOR GROUP 1')

        self.course = self.process_xml(course)
        self.course_sequence = self.course.get_children()[0]
        self.module_system = get_test_system()

        def get_module(descriptor):
            """Mocks module_system get_module function"""
            module_system = get_test_system()
            module_system.get_module = get_module
            descriptor.bind_for_student(module_system, descriptor._field_data)  # pylint: disable=protected-access
            return descriptor

        self.module_system.get_module = get_module
        self.module_system.descriptor_system = self.course.runtime
        self.course.runtime.export_fs = MemoryFS()

        self.tags_service = MemoryUserTagsService()
        self.module_system._services['user_tags'] = self.tags_service  # pylint: disable=protected-access

        self.partitions_service = StaticPartitionService(
            [
                UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha'), Group("1", 'beta')]),
                UserPartition(1, 'second_partition', 'Second Partition', [Group("0", 'abel'), Group("1", 'baker'), Group("2", 'charlie')])
            ],
            user_tags_service=self.tags_service,
            course_id=self.course.id,
            track_function=Mock(name='track_function'),
        )
        self.module_system._services['partitions'] = self.partitions_service  # pylint: disable=protected-access

        self.split_test_module = self.course_sequence.get_children()[0]
        self.split_test_module.bind_for_student(self.module_system, self.split_test_module._field_data)  # pylint: disable=protected-access


@ddt.ddt
class SplitTestModuleLMSTest(SplitTestModuleTest):
    """
    Test the split test module
    """

    @ddt.data(('0', 'split_test_cond0'), ('1', 'split_test_cond1'))
    @ddt.unpack
    def test_child(self, user_tag, child_url_name):
        self.tags_service.set_tag(
            self.tags_service.COURSE_SCOPE,
            'xblock.partition_service.partition_0',
            user_tag
        )

        self.assertEquals(self.split_test_module.child_descriptor.url_name, child_url_name)

    @ddt.data(('0',), ('1',))
    @ddt.unpack
    def test_child_old_tag_value(self, _user_tag):
        # If user_tag has a stale value, we should still get back a valid child url
        self.tags_service.set_tag(
            self.tags_service.COURSE_SCOPE,
            'xblock.partition_service.partition_0',
            '2'
        )

        self.assertIn(self.split_test_module.child_descriptor.url_name, ['split_test_cond0', 'split_test_cond1'])

    @ddt.data(('0', 'HTML FOR GROUP 0'), ('1', 'HTML FOR GROUP 1'))
    @ddt.unpack
    def test_get_html(self, user_tag, child_content):
        self.tags_service.set_tag(
            self.tags_service.COURSE_SCOPE,
            'xblock.partition_service.partition_0',
            user_tag
        )

        self.assertIn(
            child_content,
            self.module_system.render(self.split_test_module, STUDENT_VIEW).content
        )

    @ddt.data(('0',), ('1',))
    @ddt.unpack
    def test_child_missing_tag_value(self, _user_tag):
        # If user_tag has a missing value, we should still get back a valid child url
        self.assertIn(self.split_test_module.child_descriptor.url_name, ['split_test_cond0', 'split_test_cond1'])

    @ddt.data(('100',), ('200',), ('300',), ('400',), ('500',), ('600',), ('700',), ('800',), ('900',), ('1000',))
    @ddt.unpack
    def test_child_persist_new_tag_value_when_tag_missing(self, _user_tag):
        # If a user_tag has a missing value, a group should be saved/persisted for that user.
        # So, we check that we get the same url_name when we call on the url_name twice.
        # We run the test ten times so that, if our storage is failing, we'll be most likely to notice it.
        self.assertEquals(self.split_test_module.child_descriptor.url_name, self.split_test_module.child_descriptor.url_name)

    # Patch the definition_to_xml for the html children.
    @patch('xmodule.html_module.HtmlDescriptor.definition_to_xml')
    def test_export_import_round_trip(self, def_to_xml):
        # The HtmlDescriptor definition_to_xml tries to write to the filesystem
        # before returning an xml object. Patch this to just return the xml.
        def_to_xml.return_value = lxml.etree.Element('html')

        # Mock out the process_xml
        # Expect it to return a child descriptor for the SplitTestDescriptor when called.
        self.module_system.process_xml = Mock()

        # Write out the xml.
        xml_obj = self.split_test_module.definition_to_xml(MemoryFS())

        self.assertEquals(xml_obj.get('user_partition_id'), '0')
        self.assertIsNotNone(xml_obj.get('group_id_to_child'))

        # Read the xml back in.
        fields, children = SplitTestDescriptor.definition_from_xml(xml_obj, self.module_system)
        self.assertEquals(fields.get('user_partition_id'), '0')
        self.assertIsNotNone(fields.get('group_id_to_child'))
        self.assertEquals(len(children), 2)


class SplitTestModuleStudioTest(SplitTestModuleTest):
    """
    Unit tests for how split test interacts with Studio.
    """

    @patch('xmodule.split_test_module.SplitTestDescriptor.group_configuration_url', return_value='http://example.com')
    def test_render_author_view(self, group_configuration_url):
        """
        Test the rendering of the Studio author view.
        """

        def create_studio_context(root_xblock):
            """
            Context for rendering the studio "author_view".
            """
            return {
                'reorderable_items': set(),
                'root_xblock': root_xblock,
            }

        # The split_test module should render both its groups when it is the root
        context = create_studio_context(self.split_test_module)
        html = self.module_system.render(self.split_test_module, AUTHOR_VIEW, context).content
        self.assertIn('HTML FOR GROUP 0', html)
        self.assertIn('HTML FOR GROUP 1', html)

        # When rendering as a child, it shouldn't render either of its groups
        context = create_studio_context(self.course_sequence)
        html = self.module_system.render(self.split_test_module, AUTHOR_VIEW, context).content
        self.assertNotIn('HTML FOR GROUP 0', html)
        self.assertNotIn('HTML FOR GROUP 1', html)

        # The "Create Missing Groups" button should be rendered when groups are missing
        context = create_studio_context(self.split_test_module)
        self.split_test_module.user_partitions = [
            UserPartition(0, 'first_partition', 'First Partition',
                          [Group("0", 'alpha'), Group("1", 'beta'), Group("2", 'gamma')])
        ]
        html = self.module_system.render(self.split_test_module, AUTHOR_VIEW, context).content
        self.assertIn('HTML FOR GROUP 0', html)
        self.assertIn('HTML FOR GROUP 1', html)

    def test_group_configuration_url(self):
        """
        Test creation of correct Group Configuration URL.
        """
        mocked_course = Mock(advanced_modules=['split_test'])
        mocked_modulestore = Mock()
        mocked_modulestore.get_course.return_value = mocked_course
        self.split_test_module.system.modulestore = mocked_modulestore

        self.split_test_module.user_partitions = [
            UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha'), Group("1", 'beta')])
        ]

        expected_url = '/group_configurations/edX/xml_test_course/101#0'
        self.assertEqual(expected_url, self.split_test_module.group_configuration_url)

    def test_editable_settings(self):
        """
        Test the setting information passed back from editable_metadata_fields.
        """
        editable_metadata_fields = self.split_test_module.editable_metadata_fields
        self.assertIn(SplitTestDescriptor.display_name.name, editable_metadata_fields)
        self.assertNotIn(SplitTestDescriptor.due.name, editable_metadata_fields)
        self.assertNotIn(SplitTestDescriptor.user_partitions.name, editable_metadata_fields)

        # user_partition_id will always appear in editable_metadata_settings, regardless
        # of the selected value.
        self.assertIn(SplitTestDescriptor.user_partition_id.name, editable_metadata_fields)

    def test_non_editable_settings(self):
        """
        Test the settings that are marked as "non-editable".
        """
        non_editable_metadata_fields = self.split_test_module.non_editable_metadata_fields
        self.assertIn(SplitTestDescriptor.due, non_editable_metadata_fields)
        self.assertIn(SplitTestDescriptor.user_partitions, non_editable_metadata_fields)
        self.assertNotIn(SplitTestDescriptor.display_name, non_editable_metadata_fields)

    def test_available_partitions(self):
        """
        Tests that the available partitions are populated correctly when editable_metadata_fields are called
        """
        self.assertEqual([], SplitTestDescriptor.user_partition_id.values)

        # user_partitions is empty, only the "Not Selected" item will appear.
        self.split_test_module.user_partition_id = SplitTestFields.no_partition_selected['value']
        self.split_test_module.editable_metadata_fields  # pylint: disable=pointless-statement
        partitions = SplitTestDescriptor.user_partition_id.values
        self.assertEqual(1, len(partitions))
        self.assertEqual(SplitTestFields.no_partition_selected['value'], partitions[0]['value'])

        # Populate user_partitions and call editable_metadata_fields again
        self.split_test_module.user_partitions = [
            UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha'), Group("1", 'beta')])
        ]
        self.split_test_module.editable_metadata_fields  # pylint: disable=pointless-statement
        partitions = SplitTestDescriptor.user_partition_id.values
        self.assertEqual(2, len(partitions))
        self.assertEqual(SplitTestFields.no_partition_selected['value'], partitions[0]['value'])
        self.assertEqual(0, partitions[1]['value'])
        self.assertEqual("first_partition", partitions[1]['display_name'])

        # Try again with a selected partition and verify that there is no option for "No Selection"
        self.split_test_module.user_partition_id = 0
        self.split_test_module.editable_metadata_fields  # pylint: disable=pointless-statement
        partitions = SplitTestDescriptor.user_partition_id.values
        self.assertEqual(1, len(partitions))
        self.assertEqual(0, partitions[0]['value'])
        self.assertEqual("first_partition", partitions[0]['display_name'])

        # Finally try again with an invalid selected partition and verify that "No Selection" is an option
        self.split_test_module.user_partition_id = 999
        self.split_test_module.editable_metadata_fields  # pylint: disable=pointless-statement
        partitions = SplitTestDescriptor.user_partition_id.values
        self.assertEqual(2, len(partitions))
        self.assertEqual(SplitTestFields.no_partition_selected['value'], partitions[0]['value'])
        self.assertEqual(0, partitions[1]['value'])
        self.assertEqual("first_partition", partitions[1]['display_name'])

    def test_active_and_inactive_children(self):
        """
        Tests the active and inactive children returned for different split test configurations.
        """
        split_test_module = self.split_test_module
        children = split_test_module.get_children()

        # Verify that a split test has no active children if it has no specified user partition.
        split_test_module.user_partition_id = -1
        [active_children, inactive_children] = split_test_module.active_and_inactive_children()
        self.assertEqual(active_children, [])
        self.assertEqual(inactive_children, children)

        # Verify that all the children are returned as active for a correctly configured split_test
        split_test_module.user_partition_id = 0
        split_test_module.user_partitions = [
            UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha'), Group("1", 'beta')])
        ]
        [active_children, inactive_children] = split_test_module.active_and_inactive_children()
        self.assertEqual(active_children, children)
        self.assertEqual(inactive_children, [])

        # Verify that a split_test does not return inactive children in the active children
        self.split_test_module.user_partitions = [
            UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha')])
        ]
        [active_children, inactive_children] = split_test_module.active_and_inactive_children()
        self.assertEqual(active_children, [children[0]])
        self.assertEqual(inactive_children, [children[1]])

        # Verify that a split_test ignores misconfigured children
        self.split_test_module.user_partitions = [
            UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha'), Group("2", 'gamma')])
        ]
        [active_children, inactive_children] = split_test_module.active_and_inactive_children()
        self.assertEqual(active_children, [children[0]])
        self.assertEqual(inactive_children, [children[1]])

        # Verify that a split_test referring to a non-existent user partition has no active children
        self.split_test_module.user_partition_id = 2
        [active_children, inactive_children] = split_test_module.active_and_inactive_children()
        self.assertEqual(active_children, [])
        self.assertEqual(inactive_children, children)

    def test_validation_messages(self):
        """
        Test the validation messages produced for different split test configurations.
        """
        split_test_module = self.split_test_module

        def verify_validation_message(message, expected_message, expected_message_type,
                                      expected_action_class=None, expected_action_label=None,
                                      expected_action_runtime_event=None):
            """
            Verify that the validation message has the expected validation message and type.
            """
            self.assertEqual(message.text, expected_message)
            self.assertEqual(message.type, expected_message_type)
            if expected_action_class:
                self.assertEqual(message.action_class, expected_action_class)
            else:
                self.assertFalse(hasattr(message, "action_class"))
            if expected_action_label:
                self.assertEqual(message.action_label, expected_action_label)
            else:
                self.assertFalse(hasattr(message, "action_label"))
            if expected_action_runtime_event:
                self.assertEqual(message.action_runtime_event, expected_action_runtime_event)
            else:
                self.assertFalse(hasattr(message, "action_runtime_event"))

        def verify_summary_message(general_validation, expected_message, expected_message_type):
            """
            Verify that the general validation message has the expected validation message and type.
            """
            self.assertEqual(general_validation.text, expected_message)
            self.assertEqual(general_validation.type, expected_message_type)

        # Verify the messages for an unconfigured user partition
        split_test_module.user_partition_id = -1
        validation = split_test_module.validate()
        self.assertEqual(len(validation.messages), 0)
        verify_validation_message(
            validation.summary,
            u"The experiment is not associated with a group configuration.",
            StudioValidationMessage.NOT_CONFIGURED,
            'edit-button',
            u"Select a Group Configuration",
        )

        # Verify the messages for a correctly configured split_test
        split_test_module.user_partition_id = 0
        split_test_module.user_partitions = [
            UserPartition(0, 'first_partition', 'First Partition', [Group("0", 'alpha'), Group("1", 'beta')])
        ]
        validation = split_test_module.validate_split_test()
        self.assertTrue(validation)
        self.assertIsNone(split_test_module.general_validation_message(), None)

        # Verify the messages for a split test with too few groups
        split_test_module.user_partitions = [
            UserPartition(0, 'first_partition', 'First Partition',
                          [Group("0", 'alpha'), Group("1", 'beta'), Group("2", 'gamma')])
        ]
        validation = split_test_module.validate()
        self.assertEqual(len(validation.messages), 1)
        verify_validation_message(
            validation.messages[0],
            u"The experiment does not contain all of the groups in the configuration.",
            StudioValidationMessage.ERROR,
            expected_action_runtime_event='add-missing-groups',
            expected_action_label=u"Add Missing Groups"
        )
        verify_summary_message(
            validation.summary,
            u"This content experiment has issues that affect content visibility.",
            StudioValidationMessage.ERROR
        )
        # Verify the messages for a split test with children that are not associated with any group
        split_test_module.user_partitions = [
            UserPartition(0, 'first_partition', 'First Partition',
                          [Group("0", 'alpha')])
        ]
        validation = split_test_module.validate()
        self.assertEqual(len(validation.messages), 1)
        verify_validation_message(
            validation.messages[0],
            u"The experiment has an inactive group. Move content into active groups, then delete the inactive group.",
            StudioValidationMessage.WARNING
        )
        verify_summary_message(
            validation.summary,
            u"This content experiment has issues that affect content visibility.",
            StudioValidationMessage.WARNING
        )
        # Verify the messages for a split test with both missing and inactive children
        split_test_module.user_partitions = [
            UserPartition(0, 'first_partition', 'First Partition',
                          [Group("0", 'alpha'), Group("2", 'gamma')])
        ]
        validation = split_test_module.validate()
        self.assertEqual(len(validation.messages), 2)
        verify_validation_message(
            validation.messages[0],
            u"The experiment does not contain all of the groups in the configuration.",
            StudioValidationMessage.ERROR,
            expected_action_runtime_event='add-missing-groups',
            expected_action_label=u"Add Missing Groups"
        )
        verify_validation_message(
            validation.messages[1],
            u"The experiment has an inactive group. Move content into active groups, then delete the inactive group.",
            StudioValidationMessage.WARNING
        )
        # With two messages of type error and warning priority given to error.
        verify_summary_message(
            validation.summary,
            u"This content experiment has issues that affect content visibility.",
            StudioValidationMessage.ERROR
        )

        # Verify the messages for a split test referring to a non-existent user partition
        split_test_module.user_partition_id = 2
        validation = split_test_module.validate()
        self.assertEqual(len(validation.messages), 1)
        verify_validation_message(
            validation.messages[0],
            u"The experiment uses a deleted group configuration. "
            u"Select a valid group configuration or delete this experiment.",
            StudioValidationMessage.ERROR
        )
        verify_summary_message(
            validation.summary,
            u"This content experiment has issues that affect content visibility.",
            StudioValidationMessage.ERROR
        )
