# -*- coding: utf-8 -*-
"""
Tests for the student training step in the Open Assessment XBlock.
"""
import datetime
import json
import pprint

import ddt
from mock import Mock, patch
import pytz

from django.db import DatabaseError

from openassessment.assessment.models import StudentTrainingWorkflow
from openassessment.workflow import api as workflow_api
from openassessment.workflow.errors import AssessmentWorkflowError

from .base import XBlockHandlerTestCase, scenario


class StudentTrainingTest(XBlockHandlerTestCase):
    """
    Base class for student training tests.
    """

    SUBMISSION = {
        'submission': u'Thé őbjéćt őf édúćátíőń íś tő téáćh úś tő ĺővé ẃhát íś béáútífúĺ.'
    }

    def assert_path_and_context(self, xblock, expected_path, expected_context):
        """
        Render the student training step and verify that the expected template
        and context were used.  Also check that the template renders without error.

        Args:
            xblock (OpenAssessmentBlock): The XBlock under test.
            expected_path (str): The expected template path.
            expected_context (dict): The expected template context.

        Raises:
            AssertionError

        """
        path, context = xblock.training_path_and_context()

        expected_context['xblock_id'] = xblock.scope_ids.usage_id

        self.assertEqual(path, expected_path)
        self.assertEqual(len(context), len(expected_context))
        for key in expected_context.keys():
            if key == 'training_due':
                iso_date = context['training_due'].isoformat()
                self.assertEqual(iso_date, expected_context[key])
            else:
                self.assertEqual(context[key], expected_context[key])

        # Verify that we render without error
        resp = self.request(xblock, 'render_student_training', json.dumps({}))
        self.assertGreater(len(resp), 0)


@ddt.ddt
class StudentTrainingAssessTest(StudentTrainingTest):
    """
    Tests for student training assessment.
    """

    @scenario('data/student_training.xml', user_id="Plato")
    @ddt.file_data('data/student_training_mixin.json')
    def test_correct(self, xblock, data):
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        data["expected_context"]['user_timezone'] = None
        data["expected_context"]['user_language'] = None
        self.assert_path_and_context(xblock, data["expected_template"], data["expected_context"])

        # Agree with the course author's assessment
        # (as defined in the scenario XML)
        data = {
            'options_selected': {
                'Vocabulary': 'Good',
                'Grammar': 'Excellent'
            }
        }
        resp = self.request(xblock, 'training_assess', json.dumps(data), response_format='json')

        # Expect that we were correct
        self.assertTrue(resp['success'], msg=resp.get('msg'))
        self.assertFalse(resp['corrections'])

    @scenario('data/student_training.xml', user_id="Plato")
    @ddt.file_data('data/student_training_mixin.json')
    def test_correct_with_error(self, xblock, data):
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        data["expected_context"]['user_timezone'] = None
        data["expected_context"]['user_language'] = None
        self.assert_path_and_context(xblock, data["expected_template"], data["expected_context"])

        # Agree with the course author's assessment
        # (as defined in the scenario XML)
        data = {
            'options_selected': {
                'Vocabulary': 'Good',
                'Grammar': 'Excellent'
            }
        }
        with patch.object(workflow_api, "update_from_assessments") as mock_workflow_update:
            mock_workflow_update.side_effect = AssessmentWorkflowError("Oh no!")
            resp = self.request(xblock, 'training_assess', json.dumps(data), response_format='json')

            # Expect that we were not correct due to a workflow update error.
            self.assertFalse(resp['success'], msg=resp.get('msg'))
            self.assertEquals('Could not update workflow status.', resp.get('msg'))
            self.assertFalse('corrections' in resp)

    @scenario('data/student_training.xml', user_id="Plato")
    @ddt.file_data('data/student_training_mixin.json')
    def test_incorrect(self, xblock, data):
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        data["expected_context"]['user_timezone'] = None
        data["expected_context"]['user_language'] = None
        self.assert_path_and_context(xblock, data["expected_template"], data["expected_context"])

        # Disagree with the course author's assessment
        # (as defined in the scenario XML)
        select_data = {
            'options_selected': {
                'Vocabulary': 'Poor',
                'Grammar': 'Poor'
            }
        }
        resp = self.request(xblock, 'training_assess', json.dumps(select_data), response_format='json')

        # Expect that we were marked incorrect
        self.assertTrue(resp['success'], msg=resp.get('msg'))
        self.assertTrue(resp['corrections'])

    @scenario('data/student_training.xml', user_id="Plato")
    @ddt.file_data('data/student_training_mixin.json')
    def test_updates_workflow(self, xblock, data):
        expected_context = data["expected_context"].copy()
        expected_template = data["expected_template"]
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        expected_context['user_timezone'] = None
        expected_context['user_language'] = None
        self.assert_path_and_context(xblock, expected_template, expected_context)

        # Agree with the course author's assessment
        # (as defined in the scenario XML)
        selected_data = {
            'options_selected': {
                'Vocabulary': 'Good',
                'Grammar': 'Excellent'
            }
        }
        resp = self.request(xblock, 'training_assess', json.dumps(selected_data), response_format='json')

        # Expect that we were correct
        self.assertTrue(resp['success'], msg=resp.get('msg'))
        self.assertFalse(resp['corrections'])

        # Agree with the course author's assessment
        # (as defined in the scenario XML)
        selected_data = {
            'options_selected': {
                'Vocabulary': 'Excellent',
                'Grammar': 'Poor'
            }
        }

        expected_context["training_num_completed"] = 1
        expected_context["training_num_current"] = 2
        expected_context["training_essay"] = {
            'answer': {
                'parts': [{
                    'text': u"тєѕт αηѕωєя",
                    'prompt': {
                        'description':
                            u'Given the state of the world today, what do you think should be done to combat poverty?'
                    }
                }]
            }
        }

        self.assert_path_and_context(xblock, expected_template, expected_context)
        resp = self.request(xblock, 'training_assess', json.dumps(selected_data), response_format='json')

        # Expect that we were correct
        self.assertTrue(resp['success'], msg=resp.get('msg'))
        self.assertFalse(resp['corrections'])
        expected_context = {
            "allow_latex": False,
            'prompts_type': 'text',
            'user_timezone': None,
            'user_language': None
        }
        expected_template = "openassessmentblock/student_training/student_training_complete.html"
        self.assert_path_and_context(xblock, expected_template, expected_context)

    @scenario('data/feedback_only_criterion_student_training.xml', user_id='Bob')
    def test_feedback_only_criterion(self, xblock):
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        self.request(xblock, 'render_student_training', json.dumps({}))

        # Agree with the course author's assessment
        # (as defined in the scenario XML)
        # We do NOT pass in an option for the feedback-only criterion,
        # because it doesn't have any options.
        data = {
            'options_selected': {
                'vocabulary': 'good',
            }
        }
        resp = self.request(xblock, 'training_assess', json.dumps(data), response_format='json')

        # Expect that we were correct
        self.assertTrue(resp['success'], msg=resp.get('msg'))
        self.assertFalse(resp['corrections'])

    @scenario('data/student_training.xml', user_id="Plato")
    @ddt.file_data('data/student_training_mixin.json')
    def test_request_error(self, xblock, data):
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        expected_context = data["expected_context"].copy()
        expected_template = data["expected_template"]
        expected_context['user_timezone'] = None
        expected_context['user_language'] = None

        self.assert_path_and_context(xblock, expected_template, expected_context)
        resp = self.request(xblock, 'training_assess', json.dumps({}), response_format='json')
        self.assertFalse(resp['success'], msg=resp.get('msg'))

        selected_data = {
            'options_selected': "foo"
        }
        resp = self.request(xblock, 'training_assess', json.dumps(selected_data), response_format='json')
        self.assertFalse(resp['success'], msg=resp.get('msg'))

    @scenario('data/student_training.xml', user_id="Plato")
    @ddt.file_data('data/student_training_mixin.json')
    def test_invalid_options_dict(self, xblock, data):
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        expected_context = data["expected_context"].copy()
        expected_template = data["expected_template"]
        expected_context['user_timezone'] = None
        expected_context['user_language'] = None
        self.assert_path_and_context(xblock, expected_template, expected_context)

        selected_data = {
            'options_selected': {
                'Bananas': 'Excellent',
                'Grammar': 'Poor'
            }
        }

        resp = self.request(xblock, 'training_assess', json.dumps(selected_data), response_format='json')
        self.assertFalse(resp['success'], msg=resp.get('msg'))

    @scenario('data/student_training.xml', user_id="Plato")
    def test_no_submission(self, xblock):
        selected_data = {
            'options_selected': {
                'Vocabulary': 'Excellent',
                'Grammar': 'Poor'
            }
        }
        resp = self.request(xblock, 'training_assess', json.dumps(selected_data))
        self.assertIn("Your scores could not be checked", resp.decode('utf-8'))

    def _assert_path_and_context(self, xblock, expected_path, expected_context):
        """
        Render the student training step and verify that the expected template
        and context were used.  Also check that the template renders without error.

        Args:
            xblock (OpenAssessmentBlock): The XBlock under test.
            expected_path (str): The expected template path.
            expected_context (dict): The expected template context.

        Raises:
            AssertionError

        """
        path, context = xblock.training_path_and_context()

        expected_context['xblock_id'] = xblock.scope_ids.usage_id

        self.assertEqual(path, expected_path)
        self.assertEqual(len(context), len(expected_context))
        for key in expected_context.keys():
            if key == 'training_due':
                iso_date = context['training_due'].isoformat()
                self.assertEqual(iso_date, expected_context[key])
            else:
                msg = u"Expected \n {expected} \n but found \n {actual}".format(
                    actual=pprint.pformat(context[key]),
                    expected=pprint.pformat(expected_context[key])
                )
                self.assertEqual(context[key], expected_context[key], msg=msg)

            # Verify that we render without error
        resp = self.request(xblock, 'render_student_training', json.dumps({}))
        self.assertGreater(len(resp), 0)


class StudentTrainingRenderTest(StudentTrainingTest):
    """
    Tests for student training step rendering.
    """
    @scenario('data/basic_scenario.xml', user_id="Plato")
    def test_no_student_training_defined(self, xblock):
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        resp = self.request(xblock, 'render_student_training', json.dumps({}))
        self.assertEquals("", resp.decode('utf-8'))

    @scenario('data/student_training.xml', user_id="Plato")
    def test_no_submission(self, xblock):
        resp = self.request(xblock, 'render_student_training', json.dumps({}))
        self.assertIn("Not Available", resp.decode('utf-8'))

    @scenario('data/student_training.xml')
    def test_studio_preview(self, xblock):
        resp = self.request(xblock, 'render_student_training', json.dumps({}))
        self.assertIn("Not Available", resp.decode('utf-8'))

    @scenario('data/student_training_due.xml', user_id="Plato")
    def test_past_due(self, xblock):
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        expected_template = "openassessmentblock/student_training/student_training_closed.html"
        expected_context = {
            'training_due': "2000-01-01T00:00:00+00:00",
            'allow_latex': False,
            'prompts_type': 'text',
            'user_timezone': None,
            'user_language': None
        }
        self.assert_path_and_context(xblock, expected_template, expected_context)

    @scenario('data/student_training.xml', user_id="Plato")
    def test_cancelled_submission(self, xblock):
        submission = xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        xblock.get_workflow_info = Mock(return_value={
            'status': 'cancelled',
            'submission_uuid': submission['uuid']
        })
        expected_template = "openassessmentblock/student_training/student_training_cancelled.html"
        expected_context = {
            'allow_latex': False,
            'prompts_type': 'text',
            'user_timezone': None,
            'user_language': None
        }
        self.assert_path_and_context(xblock, expected_template, expected_context)

    @scenario('data/student_training.xml', user_id="Plato")
    @patch.object(StudentTrainingWorkflow, "get_workflow")
    def test_internal_error(self, xblock, mock_workflow):
        mock_workflow.side_effect = DatabaseError("Oh no.")
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        resp = self.request(xblock, 'render_student_training', json.dumps({}))
        self.assertIn("An unexpected error occurred.", resp.decode('utf-8'))

    @scenario('data/student_training_future.xml', user_id="Plato")
    def test_before_start(self, xblock):
        xblock.create_submission(xblock.get_student_item_dict(), self.SUBMISSION)
        expected_template = "openassessmentblock/student_training/student_training_unavailable.html"
        expected_context = {
            'training_start': datetime.datetime(3000, 1, 1).replace(tzinfo=pytz.utc),
            'allow_latex': False,
            'prompts_type': 'text',
            'user_timezone': None,
            'user_language': None
        }
        self.assert_path_and_context(xblock, expected_template, expected_context)