"""
Tests for signal handling in commerce djangoapp.
"""
from django.contrib.auth.models import AnonymousUser
from django.test import TestCase
from django.test.utils import override_settings

from course_modes.models import CourseMode
import ddt
import mock
from opaque_keys.edx.keys import CourseKey
from student.models import UNENROLL_DONE
from student.tests.factories import UserFactory, CourseEnrollmentFactory

from commerce.signals import refund_seat, send_refund_notification
from commerce.tests import TEST_PUBLIC_URL_ROOT, TEST_API_URL, TEST_API_SIGNING_KEY
from commerce.tests.mocks import mock_create_refund


@ddt.ddt
@override_settings(
    ECOMMERCE_PUBLIC_URL_ROOT=TEST_PUBLIC_URL_ROOT,
    ECOMMERCE_API_URL=TEST_API_URL,
    ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY,
)
class TestRefundSignal(TestCase):
    """
    Exercises logic triggered by the UNENROLL_DONE signal.
    """

    def setUp(self):
        super(TestRefundSignal, self).setUp()
        self.requester = UserFactory(username="test-requester")
        self.student = UserFactory(username="test-student", email="test-student@example.com")
        self.course_enrollment = CourseEnrollmentFactory(
            user=self.student,
            course_id=CourseKey.from_string('course-v1:org+course+run'),
            mode=CourseMode.VERIFIED,
        )
        self.course_enrollment.refundable = mock.Mock(return_value=True)

    def send_signal(self, skip_refund=False):
        """
        DRY helper: emit the UNENROLL_DONE signal, as is done in
        common.djangoapps.student.models after a successful unenrollment.
        """
        UNENROLL_DONE.send(sender=None, course_enrollment=self.course_enrollment, skip_refund=skip_refund)

    @override_settings(
        ECOMMERCE_PUBLIC_URL_ROOT=None,
        ECOMMERCE_API_URL=None,
        ECOMMERCE_API_SIGNING_KEY=None,
    )
    def test_no_service(self):
        """
        Ensure that the receiver quietly bypasses attempts to initiate
        refunds when there is no external service configured.
        """
        with mock.patch('commerce.signals.refund_seat') as mock_refund_seat:
            self.send_signal()
            self.assertFalse(mock_refund_seat.called)

    @mock.patch('commerce.signals.refund_seat')
    def test_receiver(self, mock_refund_seat):
        """
        Ensure that the UNENROLL_DONE signal triggers correct calls to
        refund_seat(), when it is appropriate to do so.

        TODO (jsa): ideally we would assert that the signal receiver got wired
        up independently of the import statement in this module.  I'm not aware
        of any reliable / sane way to do this.
        """
        self.send_signal()
        self.assertTrue(mock_refund_seat.called)
        self.assertEqual(mock_refund_seat.call_args[0], (self.course_enrollment, self.student))

        # if skip_refund is set to True in the signal, we should not try to initiate a refund.
        mock_refund_seat.reset_mock()
        self.send_signal(skip_refund=True)
        self.assertFalse(mock_refund_seat.called)

        # if the course_enrollment is not refundable, we should not try to initiate a refund.
        mock_refund_seat.reset_mock()
        self.course_enrollment.refundable = mock.Mock(return_value=False)
        self.send_signal()
        self.assertFalse(mock_refund_seat.called)

    @mock.patch('commerce.signals.refund_seat')
    @mock.patch('commerce.signals.get_request_user', return_value=None)
    def test_requester(self, mock_get_request_user, mock_refund_seat):
        """
        Ensure the right requester is specified when initiating refunds.
        """
        # no HTTP request/user: auth to commerce service as the unenrolled student.
        self.send_signal()
        self.assertTrue(mock_refund_seat.called)
        self.assertEqual(mock_refund_seat.call_args[0], (self.course_enrollment, self.student))

        # HTTP user is the student: auth to commerce service as the unenrolled student.
        mock_get_request_user.return_value = self.student
        mock_refund_seat.reset_mock()
        self.send_signal()
        self.assertTrue(mock_refund_seat.called)
        self.assertEqual(mock_refund_seat.call_args[0], (self.course_enrollment, self.student))

        # HTTP user is another user: auth to commerce service as the requester.
        mock_get_request_user.return_value = self.requester
        mock_refund_seat.reset_mock()
        self.send_signal()
        self.assertTrue(mock_refund_seat.called)
        self.assertEqual(mock_refund_seat.call_args[0], (self.course_enrollment, self.requester))

        # HTTP user is another server (AnonymousUser): do not try to initiate a refund at all.
        mock_get_request_user.return_value = AnonymousUser()
        mock_refund_seat.reset_mock()
        self.send_signal()
        self.assertFalse(mock_refund_seat.called)

    @mock.patch('commerce.signals.log.warning')
    def test_not_authorized_warning(self, mock_log_warning):
        """
        Ensure that expected authorization issues are logged as warnings.
        """
        with mock_create_refund(status=403):
            refund_seat(self.course_enrollment, UserFactory())
            self.assertTrue(mock_log_warning.called)

    @mock.patch('commerce.signals.log.exception')
    def test_error_logging(self, mock_log_exception):
        """
        Ensure that unexpected Exceptions are logged as errors (but do not
        break program flow).
        """
        with mock_create_refund(status=500):
            self.send_signal()
            self.assertTrue(mock_log_exception.called)

    @mock.patch('commerce.signals.send_refund_notification')
    def test_notification(self, mock_send_notification):
        """
        Ensure the notification function is triggered when refunds are
        initiated
        """
        with mock_create_refund(status=200, response=[1, 2, 3]):
            self.send_signal()
            self.assertTrue(mock_send_notification.called)

    @mock.patch('commerce.signals.send_refund_notification')
    def test_notification_no_refund(self, mock_send_notification):
        """
        Ensure the notification function is NOT triggered when no refunds are
        initiated
        """
        with mock_create_refund(status=200, response=[]):
            self.send_signal()
            self.assertFalse(mock_send_notification.called)

    @mock.patch('commerce.signals.send_refund_notification')
    @ddt.data(
        CourseMode.HONOR,
        CourseMode.PROFESSIONAL,
        CourseMode.AUDIT,
        CourseMode.NO_ID_PROFESSIONAL_MODE,
        CourseMode.CREDIT_MODE,
    )
    def test_notification_not_verified(self, mode, mock_send_notification):
        """
        Ensure the notification function is NOT triggered when the
        unenrollment is for any mode other than verified (i.e. any mode other
        than one for which refunds are presently supported).  See the
        TODO associated with XCOM-371 in the signals module in the commerce
        package for more information.
        """
        self.course_enrollment.mode = mode
        with mock_create_refund(status=200, response=[1, 2, 3]):
            self.send_signal()
            self.assertFalse(mock_send_notification.called)

    @mock.patch('commerce.signals.send_refund_notification', side_effect=Exception("Splat!"))
    @mock.patch('commerce.signals.log.warning')
    def test_notification_error(self, mock_log_warning, mock_send_notification):
        """
        Ensure an error occuring during notification does not break program
        flow, but a warning is logged.
        """
        with mock_create_refund(status=200, response=[1, 2, 3]):
            self.send_signal()
            self.assertTrue(mock_send_notification.called)
            self.assertTrue(mock_log_warning.called)

    @mock.patch('commerce.signals.microsite.is_request_in_microsite', return_value=True)
    def test_notification_microsite(self, mock_is_request_in_microsite):  # pylint: disable=unused-argument
        """
        Ensure the notification function raises an Exception if used in the
        context of microsites.
        """
        with self.assertRaises(NotImplementedError):
            send_refund_notification(self.course_enrollment, [1, 2, 3])

    @override_settings(PAYMENT_SUPPORT_EMAIL='payment@example.com')
    @mock.patch('commerce.signals.EmailMultiAlternatives')
    def test_notification_content(self, mock_email_class):
        """
        Ensure the email sender, recipient, subject, content type, and content
        are all correct.
        """
        # mock_email_class is the email message class/constructor.
        # mock_message is the instance returned by the constructor.
        # we need to make assertions regarding both.
        mock_message = mock.MagicMock()
        mock_email_class.return_value = mock_message

        refund_ids = [1, 2, 3]
        send_refund_notification(self.course_enrollment, refund_ids)

        # check headers and text content
        self.assertEqual(
            mock_email_class.call_args[0],
            ("[Refund] User-Requested Refund", mock.ANY, self.student.email, ['payment@example.com']),
        )
        text_body = mock_email_class.call_args[0][1]
        # check for a URL for each refund
        for exp in [r'{0}/dashboard/refunds/{1}/'.format(TEST_PUBLIC_URL_ROOT, refund_id)
                    for refund_id in refund_ids]:
            self.assertRegexpMatches(text_body, exp)

        # check HTML content
        self.assertEqual(mock_message.attach_alternative.call_args[0], (mock.ANY, "text/html"))
        html_body = mock_message.attach_alternative.call_args[0][0]
        # check for a link to each refund
        for exp in [r'a href="{0}/dashboard/refunds/{1}/"'.format(TEST_PUBLIC_URL_ROOT, refund_id)
                    for refund_id in refund_ids]:
            self.assertRegexpMatches(html_body, exp)

        # make sure we actually SEND the message too.
        self.assertTrue(mock_message.send.called)
