# -*- coding: utf-8 -*-
"""
Unit tests for lti_consumer.lti module
"""

import unittest

from datetime import timedelta
from mock import Mock, PropertyMock, patch
from six import text_type

from django.utils import timezone

from lti_consumer.tests.unit.test_utils import FAKE_USER_ID, make_request
from lti_consumer.tests.unit.test_lti_consumer import TestLtiConsumerXBlock

from lti_consumer.lti import parse_result_json, LtiConsumer
from lti_consumer.exceptions import LtiError


INVALID_JSON_INPUTS = [
    ([
        u"kk",   # ValueError
        u"{{}",  # ValueError
        u"{}}",  # ValueError
        3,       # TypeError
        {},      # TypeError
    ], u"Supplied JSON string in request body could not be decoded"),
    ([
        u"3",        # valid json, not array or object
        u"[]",       # valid json, array too small
        u"[3, {}]",  # valid json, 1st element not an object
    ], u"Supplied JSON string is a list that does not contain an object as the first element"),
    ([
        u'{"@type": "NOTResult"}',  # @type key must have value 'Result'
    ], u"JSON object does not contain correct @type attribute"),
    ([
        # @context missing
        u'{"@type": "Result", "resultScore": 0.1}',
    ], u"JSON object does not contain required key"),
    ([
        u'''
        {"@type": "Result",
         "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
         "resultScore": 100}'''  # score out of range
    ], u"score value outside the permitted range of 0.0-1.0."),
    ([
        u'''
        {"@type": "Result",
         "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
         "resultScore": -2}'''  # score out of range
    ], u"score value outside the permitted range of 0.0-1.0."),
    ([
        u'''
        {"@type": "Result",
         "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
         "resultScore": "1b"}''',   # score ValueError
        u'''
        {"@type": "Result",
         "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
         "resultScore": {}}''',   # score TypeError
    ], u"Could not convert resultScore to float"),
]

VALID_JSON_INPUTS = [
    (u'''
    {"@type": "Result",
     "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
     "resultScore": 0.1}''', 0.1, u""),  # no comment means we expect ""
    (u'''
    [{"@type": "Result",
     "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
     "@id": "anon_id:abcdef0123456789",
     "resultScore": 0.1}]''', 0.1, u""),  # OK to have array of objects -- just take the first.  @id is okay too
    (u'''
    {"@type": "Result",
     "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
     "resultScore": 0.1,
     "comment": "ಠ益ಠ"}''', 0.1, u"ಠ益ಠ"),  # unicode comment
    (u'''
    {"@type": "Result",
     "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result"}''', None, u""),  # no score means we expect None
    (u'''
    {"@type": "Result",
     "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
     "resultScore": 0.0}''', 0.0, u""),  # test lower score boundary
    (u'''
    {"@type": "Result",
     "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
     "resultScore": 1.0}''', 1.0, u""),  # test upper score boundary
]

GET_RESULT_RESPONSE = {
    "@context": "http://purl.imsglobal.org/ctx/lis/v2/Result",
    "@type": "Result",
}


class TestParseResultJson(unittest.TestCase):
    """
    Unit tests for `lti_consumer.lti.parse_result_json`
    """

    def test_invalid_json(self):
        """
        Test invalid json raises exception
        """
        for error_inputs, error_message in INVALID_JSON_INPUTS:
            for error_input in error_inputs:
                with self.assertRaisesRegexp(LtiError, error_message):
                    parse_result_json(error_input)

    def test_valid_json(self):
        """
        Test valid json returns expected values
        """
        for json_str, expected_score, expected_comment in VALID_JSON_INPUTS:
            score, comment = parse_result_json(json_str)
            self.assertEqual(score, expected_score)
            self.assertEqual(comment, expected_comment)


class TestLtiConsumer(TestLtiConsumerXBlock):
    """
    Unit tests for LtiConsumer
    """

    def setUp(self):
        super(TestLtiConsumer, self).setUp()
        self.lti_consumer = LtiConsumer(self.xblock)

    @patch(
        'lti_consumer.lti.get_oauth_request_signature',
        Mock(return_value=(
            'OAuth oauth_nonce="fake_nonce", '
            'oauth_timestamp="fake_timestamp", oauth_version="fake_version", oauth_signature_method="fake_method", '
            'oauth_consumer_key="fake_consumer_key", oauth_signature="fake_signature"'
        ))
    )
    @patch(
        'lti_consumer.lti_consumer.LtiConsumerXBlock.prefixed_custom_parameters',
        PropertyMock(return_value={u'custom_param_1': 'custom1', u'custom_param_2': 'custom2'})
    )
    @patch(
        'lti_consumer.lti_consumer.LtiConsumerXBlock.lti_provider_key_secret',
        PropertyMock(return_value=('t', 's'))
    )
    @patch('lti_consumer.lti_consumer.LtiConsumerXBlock.user_id', PropertyMock(return_value=FAKE_USER_ID))
    def test_get_signed_lti_parameters(self):
        """
        Test `get_signed_lti_parameters` returns the correct dict
        """
        self.lti_consumer.xblock.due = timezone.now()
        self.lti_consumer.xblock.graceperiod = timedelta(days=1)

        expected_lti_parameters = {
            text_type('user_id'): self.lti_consumer.xblock.user_id,
            text_type('oauth_callback'): 'about:blank',
            text_type('launch_presentation_return_url'): '',
            text_type('lti_message_type'): 'basic-lti-launch-request',
            text_type('lti_version'): 'LTI-1p0',
            text_type('roles'): self.lti_consumer.xblock.role,
            text_type('resource_link_id'): self.lti_consumer.xblock.resource_link_id,
            text_type('lis_result_sourcedid'): self.lti_consumer.xblock.lis_result_sourcedid,
            text_type('context_id'): self.lti_consumer.xblock.context_id,
            text_type('lis_outcome_service_url'): self.lti_consumer.xblock.outcome_service_url,
            text_type('custom_component_display_name'): self.lti_consumer.xblock.display_name,
            text_type('custom_component_due_date'): self.lti_consumer.xblock.due.strftime('%Y-%m-%d %H:%M:%S'),
            text_type('custom_component_graceperiod'): str(self.lti_consumer.xblock.graceperiod.total_seconds()),
            'lis_person_sourcedid': 'edx',
            'lis_person_contact_email_primary': 'edx@example.com',
            'launch_presentation_locale': 'en',
            text_type('custom_param_1'): 'custom1',
            text_type('custom_param_2'): 'custom2',
            text_type('oauth_nonce'): 'fake_nonce',
            'oauth_timestamp': 'fake_timestamp',
            'oauth_version': 'fake_version',
            'oauth_signature_method': 'fake_method',
            'oauth_consumer_key': 'fake_consumer_key',
            'oauth_signature': 'fake_signature',
            text_type('context_label'): self.lti_consumer.xblock.course.display_org_with_default,
            text_type('context_title'): self.lti_consumer.xblock.course.display_name_with_default,
        }
        self.lti_consumer.xblock.has_score = True
        self.lti_consumer.xblock.ask_to_send_username = True
        self.lti_consumer.xblock.ask_to_send_email = True
        self.lti_consumer.xblock.runtime.get_real_user.return_value = Mock(
            email='edx@example.com',
            username='edx',
            preferences=Mock(filter=Mock(return_value=[Mock(value='en')]))
        )
        self.assertEqual(self.lti_consumer.get_signed_lti_parameters(), expected_lti_parameters)

        # Test that `lis_person_sourcedid`, `lis_person_contact_email_primary`, and `launch_presentation_locale`
        # are not included in the returned LTI parameters when a user cannot be found
        self.lti_consumer.xblock.runtime.get_real_user.return_value = {}
        del expected_lti_parameters['lis_person_sourcedid']
        del expected_lti_parameters['lis_person_contact_email_primary']
        del expected_lti_parameters['launch_presentation_locale']
        self.assertEqual(self.lti_consumer.get_signed_lti_parameters(), expected_lti_parameters)

    def test_get_result(self):
        """
        Test `get_result` returns valid json response
        """
        self.xblock.module_score = 0.9
        self.xblock.score_comment = 'Great Job!'
        response = dict(GET_RESULT_RESPONSE)
        response.update({
            "resultScore": self.xblock.module_score,
            "comment": self.xblock.score_comment
        })
        self.assertEqual(self.lti_consumer.get_result(Mock()), response)

        self.xblock.module_score = None
        self.xblock.score_comment = ''
        self.assertEqual(self.lti_consumer.get_result(Mock()), GET_RESULT_RESPONSE)

    @patch('lti_consumer.lti_consumer.LtiConsumerXBlock.clear_user_module_score')
    def test_delete_result(self, mock_clear):
        """
        Test `delete_result` calls `LtiConsumerXBlock.clear_user_module_score`
        """
        user = Mock()
        response = self.lti_consumer.delete_result(user)

        mock_clear.assert_called_with(user)
        self.assertEqual(response, {})

    @patch('lti_consumer.lti_consumer.LtiConsumerXBlock.max_score', Mock(return_value=1.0))
    @patch('lti_consumer.lti_consumer.LtiConsumerXBlock.set_user_module_score')
    @patch('lti_consumer.lti_consumer.LtiConsumerXBlock.clear_user_module_score')
    @patch('lti_consumer.lti.parse_result_json')
    def test_put_result(self, mock_parse, mock_clear, mock_set):
        """
        Test `put_result` calls `LtiConsumerXBlock.set_user_module_score`
        or `LtiConsumerXblock.clear_user_module_score` if resultScore not included in request
        """
        user = Mock()
        score = 0.9
        comment = 'Great Job!'

        mock_parse.return_value = (score, comment)
        response = self.lti_consumer.put_result(user, '')
        mock_set.assert_called_with(user, score, 1.0, comment)
        self.assertEqual(response, {})

        mock_parse.return_value = (None, '')
        response = self.lti_consumer.put_result(user, '')
        mock_clear.assert_called_with(user)
        self.assertEqual(response, {})

    @patch('lti_consumer.lti.log')
    def test_verify_result_headers_verify_content_type_true(self, mock_log):
        """
        Test wrong content type raises exception if `verify_content_type` is True
        """
        request = make_request('')

        with self.assertRaises(LtiError):
            self.lti_consumer.verify_result_headers(request)
            self.assertTrue(mock_log.called)

    @patch('lti_consumer.lti.verify_oauth_body_signature', Mock(return_value=True))
    @patch('lti_consumer.lti_consumer.LtiConsumerXBlock.lti_provider_key_secret', PropertyMock(return_value=('t', 's')))
    def test_verify_result_headers_verify_content_type_false(self):
        """
        Test content type check skipped if `verify_content_type` is False
        """
        request = make_request('')
        request.environ['CONTENT_TYPE'] = LtiConsumer.CONTENT_TYPE_RESULT_JSON
        response = self.lti_consumer.verify_result_headers(request, False)

        self.assertTrue(response)

    @patch('lti_consumer.lti.verify_oauth_body_signature', Mock(return_value=True))
    @patch('lti_consumer.lti_consumer.LtiConsumerXBlock.lti_provider_key_secret', PropertyMock(return_value=('t', 's')))
    def test_verify_result_headers_valid(self):
        """
        Test True is returned if request is valid
        """
        request = make_request('')
        request.environ['CONTENT_TYPE'] = LtiConsumer.CONTENT_TYPE_RESULT_JSON
        response = self.lti_consumer.verify_result_headers(request)

        self.assertTrue(response)

    @patch('lti_consumer.lti.verify_oauth_body_signature', Mock(side_effect=LtiError))
    @patch('lti_consumer.lti_consumer.LtiConsumerXBlock.lti_provider_key_secret', PropertyMock(return_value=('t', 's')))
    @patch('lti_consumer.lti.log')
    def test_verify_result_headers_lti_error(self, mock_log):
        """
        Test exception raised if request header verification raises error
        """
        request = make_request('')
        request.environ['CONTENT_TYPE'] = LtiConsumer.CONTENT_TYPE_RESULT_JSON

        with self.assertRaises(LtiError):
            self.lti_consumer.verify_result_headers(request)
            self.assertTrue(mock_log.called)

    @patch('lti_consumer.lti.verify_oauth_body_signature', Mock(side_effect=ValueError))
    @patch('lti_consumer.lti_consumer.LtiConsumerXBlock.lti_provider_key_secret', PropertyMock(return_value=('t', 's')))
    @patch('lti_consumer.lti.log')
    def test_verify_result_headers_value_error(self, mock_log):
        """
        Test exception raised if request header verification raises error
        """
        request = make_request('')
        request.environ['CONTENT_TYPE'] = LtiConsumer.CONTENT_TYPE_RESULT_JSON

        with self.assertRaises(LtiError):
            self.lti_consumer.verify_result_headers(request)
            self.assertTrue(mock_log.called)