# pylint: disable=invalid-name, attribute-defined-outside-init
"""
Tests for basic common operations related to Course Action State managers
"""

from collections import namedtuple

from ddt import data, ddt
from django.test import TestCase
from opaque_keys.edx.locations import CourseLocator
from six.moves import range

from course_action_state.managers import CourseActionStateItemNotFoundError
from course_action_state.models import CourseRerunState

# Sequence of Action models to be tested with ddt.
COURSE_ACTION_STATES = (CourseRerunState, )


class TestCourseActionStateManagerBase(TestCase):
    """
    Base class for testing Course Action State Managers.
    """
    def setUp(self):
        super(TestCourseActionStateManagerBase, self).setUp()
        self.course_key = CourseLocator("test_org", "test_course_num", "test_run")


@ddt
class TestCourseActionStateManager(TestCourseActionStateManagerBase):
    """
    Test class for testing the CourseActionStateManager.
    """
    @data(*COURSE_ACTION_STATES)
    def test_update_state_allow_not_found_is_false(self, action_class):
        with self.assertRaises(CourseActionStateItemNotFoundError):
            action_class.objects.update_state(self.course_key, "fake_state", allow_not_found=False)

    @data(*COURSE_ACTION_STATES)
    def test_update_state_allow_not_found(self, action_class):
        action_class.objects.update_state(self.course_key, "initial_state", allow_not_found=True)
        self.assertIsNotNone(
            action_class.objects.find_first(course_key=self.course_key)
        )

    @data(*COURSE_ACTION_STATES)
    def test_delete(self, action_class):
        obj = action_class.objects.update_state(self.course_key, "initial_state", allow_not_found=True)
        action_class.objects.delete(obj.id)
        with self.assertRaises(CourseActionStateItemNotFoundError):
            action_class.objects.find_first(course_key=self.course_key)


@ddt
class TestCourseActionUIStateManager(TestCourseActionStateManagerBase):
    """
    Test class for testing the CourseActionUIStateManager.
    """
    def init_course_action_states(self, action_class):
        """
        Creates course action state entries with different states for the given action model class.
        Creates both displayable (should_display=True) and non-displayable (should_display=False) entries.
        """
        def create_course_states(starting_course_num, ending_course_num, state, should_display=True):
            """
            Creates a list of course state tuples by creating unique course locators with course-numbers
            from starting_course_num to ending_course_num.
            """
            CourseState = namedtuple('CourseState', 'course_key, state, should_display')
            return [
                CourseState(CourseLocator("org", "course", "run" + str(num)), state, should_display)
                for num in range(starting_course_num, ending_course_num)
            ]

        NUM_COURSES_WITH_STATE1 = 3
        NUM_COURSES_WITH_STATE2 = 3
        NUM_COURSES_WITH_STATE3 = 3
        NUM_COURSES_NON_DISPLAYABLE = 3

        # courses with state1 and should_display=True
        self.courses_with_state1 = create_course_states(
            0,
            NUM_COURSES_WITH_STATE1,
            'state1'
        )
        # courses with state2 and should_display=True
        self.courses_with_state2 = create_course_states(
            NUM_COURSES_WITH_STATE1,
            NUM_COURSES_WITH_STATE1 + NUM_COURSES_WITH_STATE2,
            'state2'
        )
        # courses with state3 and should_display=True
        self.courses_with_state3 = create_course_states(
            NUM_COURSES_WITH_STATE1 + NUM_COURSES_WITH_STATE2,
            NUM_COURSES_WITH_STATE1 + NUM_COURSES_WITH_STATE2 + NUM_COURSES_WITH_STATE3,
            'state3'
        )
        # all courses with should_display=True
        self.course_actions_displayable_states = (
            self.courses_with_state1 + self.courses_with_state2 + self.courses_with_state3
        )
        # courses with state3 and should_display=False
        self.courses_with_state3_non_displayable = create_course_states(
            NUM_COURSES_WITH_STATE1 + NUM_COURSES_WITH_STATE2 + NUM_COURSES_WITH_STATE3,
            NUM_COURSES_WITH_STATE1 + NUM_COURSES_WITH_STATE2 + NUM_COURSES_WITH_STATE3 + NUM_COURSES_NON_DISPLAYABLE,
            'state3',
            should_display=False,
        )

        # create course action states for all courses
        for CourseState in self.course_actions_displayable_states + self.courses_with_state3_non_displayable:
            action_class.objects.update_state(
                CourseState.course_key,
                CourseState.state,
                should_display=CourseState.should_display,
                allow_not_found=True
            )

    def assertCourseActionStatesEqual(self, expected, found):
        """Asserts that the set of course keys in the expected state equal those that are found"""
        self.assertSetEqual(
            set(course_action_state.course_key for course_action_state in expected),
            set(course_action_state.course_key for course_action_state in found))

    @data(*COURSE_ACTION_STATES)
    def test_find_all_for_display(self, action_class):
        self.init_course_action_states(action_class)
        self.assertCourseActionStatesEqual(
            self.course_actions_displayable_states,
            action_class.objects.find_all(should_display=True),
        )

    @data(*COURSE_ACTION_STATES)
    def test_find_all_for_display_filter_exclude(self, action_class):
        self.init_course_action_states(action_class)
        for course_action_state, filter_state, exclude_state in (
            (self.courses_with_state1, 'state1', None),  # filter for state1
            (self.courses_with_state2, 'state2', None),  # filter for state2
            (self.courses_with_state2 + self.courses_with_state3, None, 'state1'),  # exclude state1
            (self.courses_with_state1 + self.courses_with_state3, None, 'state2'),  # exclude state2
            (self.courses_with_state1, 'state1', 'state2'),  # filter for state1, exclude state2
            ([], 'state1', 'state1'),  # filter for state1, exclude state1
        ):
            self.assertCourseActionStatesEqual(
                course_action_state,
                action_class.objects.find_all(
                    exclude_args=({'state': exclude_state} if exclude_state else None),
                    should_display=True,
                    **({'state': filter_state} if filter_state else {})
                )
            )

    def test_kwargs_in_update_state(self):
        destination_course_key = CourseLocator("org", "course", "run")
        source_course_key = CourseLocator("source_org", "source_course", "source_run")
        CourseRerunState.objects.update_state(
            course_key=destination_course_key,
            new_state='state1',
            allow_not_found=True,
            source_course_key=source_course_key,
        )
        found_action_state = CourseRerunState.objects.find_first(course_key=destination_course_key)
        self.assertEqual(source_course_key, found_action_state.source_course_key)
