import datetime
from itertools import product

import ddt
import mock
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory

from course_discovery.apps.api.utils import StudioAPI, cast2int, get_query_param
from course_discovery.apps.core.utils import serialize_datetime
from course_discovery.apps.course_metadata.tests.factories import CourseEditorFactory, CourseRunFactory

LOGGER_PATH = 'course_discovery.apps.api.utils.logger.exception'


@ddt.ddt
class Cast2IntTests(TestCase):
    name = 'foo'

    @ddt.data(
        ('0', 0),
        ('1', 1),
        (None, None),
    )
    @ddt.unpack
    def test_cast_success(self, value, expected):
        self.assertEqual(cast2int(value, self.name), expected)

    @ddt.data('beep', '1.1')
    def test_cast_failure(self, value):
        with mock.patch(LOGGER_PATH) as mock_logger:
            with self.assertRaises(ValueError):
                cast2int(value, self.name)

        self.assertTrue(mock_logger.called)


class TestGetQueryParam:
    def test_with_request(self):
        factory = APIRequestFactory()
        request = Request(factory.get('/?q=1'))

        assert get_query_param(request, 'q') == 1

    def test_without_request(self):
        assert get_query_param(None, 'q') is None


@ddt.ddt
class StudioAPITests(TestCase):
    def setUp(self):
        super().setUp()
        self.client = mock.Mock()
        self.api = StudioAPI(self.client)

    def make_studio_data(self, run, add_pacing=True, add_schedule=True, team=None):
        key = CourseKey.from_string(run.key)
        data = {
            'title': run.title,
            'org': key.org,
            'number': key.course,
            'run': key.run,
            'team': team or [],
        }
        if add_pacing:
            data['pacing_type'] = run.pacing_type
        if add_schedule:
            data['schedule'] = {
                'start': serialize_datetime(run.start),
                'end': serialize_datetime(run.end),
            }
        return data

    def assert_data_generated_correctly(self, course_run, expected_team_data, creating=False):
        course = course_run.course
        expected = {
            'title': course_run.title_override or course.title,
            'org': course.organizations.first().key,
            'number': course.number,
            'run': StudioAPI.calculate_course_run_key_run_value(course.number, course_run.start_date_temporary),
            'schedule': {
                'start': serialize_datetime(course_run.start_date_temporary),
                'end': serialize_datetime(course_run.end_date_temporary),
            },
            'team': expected_team_data,
            'pacing_type': course_run.pacing_type_temporary,
        }
        self.assertEqual(StudioAPI.generate_data_for_studio_api(course_run, creating=creating), expected)

    def test_create_rerun(self):
        run1 = CourseRunFactory()
        run2 = CourseRunFactory(course=run1.course)
        self.api.create_course_rerun_in_studio(run2, run1.key)

        expected_data = self.make_studio_data(run2)
        self.assertEqual(self.client.course_runs.call_args_list, [mock.call(run1.key)])
        self.assertEqual(self.client.course_runs.return_value.rerun.post.call_args_list[0][0][0], expected_data)

    def test_create_run(self):
        run = CourseRunFactory()
        self.api.create_course_run_in_studio(run)

        expected_data = self.make_studio_data(run)
        self.assertEqual(self.client.course_runs.post.call_args_list[0][0][0], expected_data)

    def test_update_run(self):
        run = CourseRunFactory()
        self.api.update_course_run_details_in_studio(run)

        expected_data = self.make_studio_data(run, add_pacing=False, add_schedule=False)
        self.assertEqual(self.client.course_runs.call_args_list, [mock.call(run.key)])
        self.assertEqual(self.client.course_runs.return_value.patch.call_args_list[0][0][0], expected_data)

    @ddt.data(
        *product(range(1, 5), ['1T2017']),
        *product(range(5, 9), ['2T2017']),
        *product(range(9, 13), ['3T2017']),
    )
    @ddt.unpack
    def test_calculate_course_run_key_run_value(self, month, expected):
        start = datetime.datetime(2017, month, 1)
        self.assertEqual(StudioAPI.calculate_course_run_key_run_value('NONE', start=start), expected)

    def test_generate_data_for_studio_api(self):
        run = CourseRunFactory()
        editor = CourseEditorFactory(course=run.course)
        team = [
            {
                'user': editor.user.username,
                'role': 'instructor',
            },
        ]
        self.assertEqual(StudioAPI.generate_data_for_studio_api(run, True), self.make_studio_data(run, team=team))

    def test_generate_data_for_studio_api_without_team(self):
        run = CourseRunFactory()
        with mock.patch('course_discovery.apps.api.utils.logger.warning') as mock_logger:
            self.assertEqual(StudioAPI.generate_data_for_studio_api(run, True), self.make_studio_data(run))
        mock_logger.assert_called_with(
            'No course team admin specified for course [%s]. This may result in a Studio course run '
            'being created without a course team.',
            run.key.split('/')[1]
        )

    def test_calculate_course_run_key_run_value_with_multiple_runs_per_trimester(self):
        start = datetime.datetime(2017, 2, 1)

        CourseRunFactory(key='course-v1:TestX+Testing101x+1T2017')
        self.assertEqual(StudioAPI.calculate_course_run_key_run_value('TestX', start), '1T2017a')

        CourseRunFactory(key='course-v1:TestX+Testing101x+1T2017a')
        self.assertEqual(StudioAPI.calculate_course_run_key_run_value('TestX', start), '1T2017b')

    def test_update_course_run_image_in_studio_without_course_image(self):
        run = CourseRunFactory(course__image=None)
        with mock.patch('course_discovery.apps.api.utils.logger') as mock_logger:
            self.api.update_course_run_image_in_studio(run)
            mock_logger.warning.assert_called_with(
                'Card image for course run [%d] cannot be updated. The related course [%d] has no image defined.',
                run.id,
                run.course.id
            )