try:
    from urllib.parse import parse_qs, urlparse
except ImportError:
    # Python < 3
    from urlparse import parse_qs, urlparse

from mock import patch

from django.core.exceptions import SuspiciousOperation
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.urls import reverse
from django.test import RequestFactory, TestCase, override_settings

from mozilla_django_oidc import views


User = get_user_model()


def my_custom_op_logout(request):
    return request.build_absolute_uri('/logged/out')


class OIDCAuthorizationCallbackViewTestCase(TestCase):
    def setUp(self):
        self.factory = RequestFactory()

    @override_settings(LOGIN_REDIRECT_URL='/success')
    def test_get_auth_success(self):
        """Test successful callback request to RP."""
        user = User.objects.create_user('example_username')

        get_data = {
            'code': 'example_code',
            'state': 'example_state'
        }
        url = reverse('oidc_authentication_callback')
        request = self.factory.get(url, get_data)
        request.session = {
            'oidc_state': 'example_state'
        }
        callback_view = views.OIDCAuthenticationCallbackView.as_view()

        with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
            with patch('mozilla_django_oidc.views.auth.login') as mock_login:
                mock_auth.return_value = user
                response = callback_view(request)

                mock_auth.assert_called_once_with(nonce=None,
                                                  request=request)
                mock_login.assert_called_once_with(request, user)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, '/success')

    @override_settings(LOGIN_REDIRECT_URL='/success')
    def test_get_auth_success_next_url(self):
        """Test successful callback request to RP with custom `next` url."""
        user = User.objects.create_user('example_username')

        get_data = {
            'code': 'example_code',
            'state': 'example_state'
        }
        url = reverse('oidc_authentication_callback')
        request = self.factory.get(url, get_data)
        request.session = {
            'oidc_state': 'example_state',
            'oidc_login_next': '/foobar'
        }
        callback_view = views.OIDCAuthenticationCallbackView.as_view()

        with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
            with patch('mozilla_django_oidc.views.auth.login') as mock_login:
                mock_auth.return_value = user
                response = callback_view(request)

                mock_auth.assert_called_once_with(nonce=None,
                                                  request=request)
                mock_login.assert_called_once_with(request, user)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, '/foobar')

    @override_settings(LOGIN_REDIRECT_URL_FAILURE='/failure')
    def test_get_auth_failure_nonexisting_user(self):
        """Test unsuccessful authentication and redirect url."""
        get_data = {
            'code': 'example_code',
            'state': 'example_state'
        }

        url = reverse('oidc_authentication_callback')
        request = self.factory.get(url, get_data)
        request.session = {
            'oidc_state': 'example_state'
        }
        callback_view = views.OIDCAuthenticationCallbackView.as_view()

        with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
            mock_auth.return_value = None
            response = callback_view(request)

            mock_auth.assert_called_once_with(nonce=None,
                                              request=request)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, '/failure')

    @override_settings(LOGIN_REDIRECT_URL_FAILURE='/failure')
    def test_get_auth_failure_inactive_user(self):
        """Test authentication failure attempt for an inactive user."""
        user = User.objects.create_user('example_username')
        user.is_active = False
        user.save()

        get_data = {
            'code': 'example_code',
            'state': 'example_state'
        }

        url = reverse('oidc_authentication_callback')
        request = self.factory.get(url, get_data)
        request.session = {
            'oidc_state': 'example_state'
        }
        callback_view = views.OIDCAuthenticationCallbackView.as_view()

        with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
            mock_auth.return_value = user
            response = callback_view(request)

            mock_auth.assert_called_once_with(request=request,
                                              nonce=None)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, '/failure')

    @override_settings(OIDC_USE_NONCE=False)
    @override_settings(LOGIN_REDIRECT_URL_FAILURE='/failure')
    def test_get_auth_dirty_data(self):
        """Test authentication attempt with wrong get data."""
        get_data = {
            'foo': 'bar',
        }

        url = reverse('oidc_authentication_callback')
        request = self.factory.get(url, get_data)
        request.session = {}
        callback_view = views.OIDCAuthenticationCallbackView.as_view()
        response = callback_view(request)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, '/failure')

    @override_settings(LOGIN_REDIRECT_URL_FAILURE='/failure')
    def test_get_auth_failure_missing_session_state(self):
        """Test authentication failure attempt for an inactive user."""
        user = User.objects.create_user('example_username')
        user.is_active = False
        user.save()

        get_data = {
            'code': 'example_code',
            'state': 'example_state'
        }

        url = reverse('oidc_authentication_callback')
        request = self.factory.get(url, get_data)
        request.session = {
        }
        callback_view = views.OIDCAuthenticationCallbackView.as_view()

        response = callback_view(request)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, '/failure')

    @override_settings(LOGIN_REDIRECT_URL_FAILURE='/failure')
    def test_get_auth_failure_tampered_session_state(self):
        """Test authentication failure attempt for an inactive user."""
        user = User.objects.create_user('example_username')
        user.is_active = False
        user.save()

        get_data = {
            'code': 'example_code',
            'state': 'example_state'
        }

        url = reverse('oidc_authentication_callback')
        request = self.factory.get(url, get_data)
        request.session = {
            'oidc_state': 'tampered_state'
        }
        callback_view = views.OIDCAuthenticationCallbackView.as_view()

        with self.assertRaises(SuspiciousOperation) as context:
            callback_view(request)

        expected_error_message = 'Session `oidc_state` does not match the OIDC callback state'
        self.assertEqual(context.exception.args, (expected_error_message,))

    @override_settings(LOGIN_REDIRECT_URL='/success')
    def test_nonce_is_deleted(self):
        """Test Nonce is not in session."""
        user = User.objects.create_user('example_username')

        get_data = {
            'code': 'example_code',
            'state': 'example_state'
        }
        url = reverse('oidc_authentication_callback')
        request = self.factory.get(url, get_data)
        request.session = {
            'oidc_state': 'example_state',
            'oidc_nonce': 'example_nonce'
        }
        callback_view = views.OIDCAuthenticationCallbackView.as_view()

        with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
            with patch('mozilla_django_oidc.views.auth.login') as mock_login:
                mock_auth.return_value = user
                response = callback_view(request)

                mock_auth.assert_called_once_with(nonce='example_nonce',
                                                  request=request)
                mock_login.assert_called_once_with(request, user)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, '/success')
        self.assertTrue('oidc_nonce' not in request.session)


class GetNextURLTestCase(TestCase):
    def setUp(self):
        self.factory = RequestFactory()

    def build_request(self, next_url):
        return self.factory.get('/', data={'next': next_url})

    def test_no_param(self):
        req = self.factory.get('/')
        next_url = views.get_next_url(req, 'next')
        self.assertEqual(next_url, None)

    def test_non_next_param(self):
        req = self.factory.get('/', data={'redirectto': '/foo'})
        next_url = views.get_next_url(req, 'redirectto')
        self.assertEqual(next_url, '/foo')

    def test_good_urls(self):
        urls = [
            '/',
            '/foo',
            '/foo?bar=baz',
            'http://testserver/foo',
        ]
        for url in urls:
            req = self.build_request(next_url=url)
            next_url = views.get_next_url(req, 'next')

            self.assertEqual(next_url, url)

    def test_bad_urls(self):
        urls = [
            '',
            # NOTE(willkg): Test data taken from the Django is_safe_url tests.
            'http://example.com',
            'http:///example.com',
            'https://example.com',
            'ftp://example.com',
            r'\\example.com',
            r'\\\example.com',
            r'/\\/example.com',
            r'\\\example.com',
            r'\\example.com',
            r'\\//example.com',
            r'/\/example.com',
            r'\/example.com',
            r'/\example.com',
            'http:///example.com',
            r'http:/\//example.com',
            r'http:\/example.com',
            r'http:/\example.com',
            'javascript:alert("XSS")',
            '\njavascript:alert(x)',
            '\x08//example.com',
            r'http://otherserver\@example.com',
            r'http:\\testserver\@example.com',
            r'http://testserver\me:pass@example.com',
            r'http://testserver\@example.com',
            r'http:\\testserver\confirm\me@example.com',
            'http:999999999',
            'ftp:9999999999',
            '\n',
        ]
        for url in urls:
            req = self.build_request(next_url=url)
            next_url = views.get_next_url(req, 'next')

            self.assertEqual(next_url, None)

    def test_https(self):
        # If the request is for HTTPS and the next url is HTTPS, then that
        # works with all Djangos.
        req = self.factory.get(
            '/',
            data={'next': 'https://testserver/foo'},
            secure=True,
        )
        self.assertEqual(req.is_secure(), True)
        next_url = views.get_next_url(req, 'next')
        self.assertEqual(next_url, 'https://testserver/foo')

        # If the request is for HTTPS and the next url is HTTP, then that fails.
        req = self.factory.get(
            '/',
            data={'next': 'http://testserver/foo'},
            secure=True,
        )
        self.assertEqual(req.is_secure(), True)
        next_url = views.get_next_url(req, 'next')
        self.assertEqual(next_url, None)

    @override_settings(OIDC_REDIRECT_REQUIRE_HTTPS=False)
    def test_redirect_https_not_required(self):
        req = self.factory.get(
            '/',
            data={'next': 'http://testserver/foo'},
            secure=True
        )

        next_url = views.get_next_url(req, 'next')
        self.assertEqual(next_url, 'http://testserver/foo')

    @override_settings(OIDC_REDIRECT_ALLOWED_HOSTS=['example.com', 'foo.com'])
    def test_redirect_allowed_hosts(self):
        req = self.factory.get(
            '/',
            data={'next': 'https://example.com/foo'},
            secure=True
        )

        next_url = views.get_next_url(req, 'next')
        self.assertEqual(next_url, 'https://example.com/foo')


class OIDCAuthorizationRequestViewTestCase(TestCase):
    def setUp(self):
        self.factory = RequestFactory()

    @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='https://server.example.com/auth')
    @override_settings(OIDC_RP_CLIENT_ID='example_id')
    @patch('mozilla_django_oidc.views.get_random_string')
    def test_get(self, mock_random_string):
        """Test initiation of a successful OIDC attempt."""
        mock_random_string.return_value = 'examplestring'
        url = reverse('oidc_authentication_init')
        request = self.factory.get(url)
        request.session = dict()
        login_view = views.OIDCAuthenticationRequestView.as_view()
        response = login_view(request)
        self.assertEqual(response.status_code, 302)

        o = urlparse(response.url)
        expected_query = {
            'response_type': ['code'],
            'scope': ['openid email'],
            'client_id': ['example_id'],
            'redirect_uri': ['http://testserver/callback/'],
            'state': ['examplestring'],
            'nonce': ['examplestring']
        }
        self.assertDictEqual(parse_qs(o.query), expected_query)
        self.assertEqual(o.hostname, 'server.example.com')
        self.assertEqual(o.path, '/auth')

    @override_settings(ROOT_URLCONF='tests.namespaced_urls')
    @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='https://server.example.com/auth')
    @override_settings(OIDC_RP_CLIENT_ID='example_id')
    @override_settings(OIDC_AUTHENTICATION_CALLBACK_URL='namespace:oidc_authentication_callback')
    @patch('mozilla_django_oidc.views.get_random_string')
    def test_get_namespaced(self, mock_random_string):
        """Test initiation of a successful OIDC attempt with namespaced redirect_uri."""
        mock_random_string.return_value = 'examplestring'
        url = reverse('namespace:oidc_authentication_init')
        request = self.factory.get(url)
        request.session = dict()
        login_view = views.OIDCAuthenticationRequestView.as_view()
        response = login_view(request)
        self.assertEqual(response.status_code, 302)

        o = urlparse(response.url)
        expected_query = {
            'response_type': ['code'],
            'scope': ['openid email'],
            'client_id': ['example_id'],
            'redirect_uri': ['http://testserver/namespace/callback/'],
            'state': ['examplestring'],
            'nonce': ['examplestring']
        }
        self.assertDictEqual(parse_qs(o.query), expected_query)
        self.assertEqual(o.hostname, 'server.example.com')
        self.assertEqual(o.path, '/auth')

    @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='https://server.example.com/auth')
    @override_settings(OIDC_RP_CLIENT_ID='example_id')
    @override_settings(OIDC_AUTH_REQUEST_EXTRA_PARAMS={'audience': 'some-api.example.com'})
    @patch('mozilla_django_oidc.views.get_random_string')
    def test_get_with_audience(self, mock_random_string):
        """Test initiation of a successful OIDC attempt."""
        mock_random_string.return_value = 'examplestring'
        url = reverse('oidc_authentication_init')
        request = self.factory.get(url)
        request.session = dict()
        login_view = views.OIDCAuthenticationRequestView.as_view()
        response = login_view(request)
        self.assertEqual(response.status_code, 302)

        o = urlparse(response.url)
        expected_query = {
            'response_type': ['code'],
            'scope': ['openid email'],
            'client_id': ['example_id'],
            'redirect_uri': ['http://testserver/callback/'],
            'state': ['examplestring'],
            'nonce': ['examplestring'],
            'audience': ['some-api.example.com'],
        }
        self.assertDictEqual(parse_qs(o.query), expected_query)
        self.assertEqual(o.hostname, 'server.example.com')
        self.assertEqual(o.path, '/auth')

    @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='https://server.example.com/auth')
    @override_settings(OIDC_RP_CLIENT_ID='example_id')
    @patch('mozilla_django_oidc.views.get_random_string')
    @patch('mozilla_django_oidc.views.OIDCAuthenticationRequestView.get_extra_params')
    def test_get_with_overridden_extra_params(self, mock_extra_params, mock_random_string):
        """Test overriding OIDCAuthenticationRequestView.get_extra_params()."""
        mock_random_string.return_value = 'examplestring'

        mock_extra_params.return_value = {
            'connection': 'foo'
        }

        url = reverse('oidc_authentication_init')
        request = self.factory.get(url)
        request.session = dict()
        login_view = views.OIDCAuthenticationRequestView.as_view()
        response = login_view(request)
        self.assertEqual(response.status_code, 302)

        o = urlparse(response.url)
        expected_query = {
            'response_type': ['code'],
            'scope': ['openid email'],
            'client_id': ['example_id'],
            'redirect_uri': ['http://testserver/callback/'],
            'state': ['examplestring'],
            'nonce': ['examplestring'],
            'connection': ['foo'],
        }
        self.assertDictEqual(parse_qs(o.query), expected_query)
        self.assertEqual(o.hostname, 'server.example.com')
        self.assertEqual(o.path, '/auth')

        mock_extra_params.assert_called_with(request)

    @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='https://server.example.com/auth')
    @override_settings(OIDC_RP_CLIENT_ID='example_id')
    def test_next_url(self):
        """Test that `next` url gets stored to user session."""
        url = reverse('oidc_authentication_init')
        request = self.factory.get('{url}?{params}'.format(url=url, params='next=/foo'))
        request.session = dict()
        login_view = views.OIDCAuthenticationRequestView.as_view()
        login_view(request)
        self.assertTrue('oidc_login_next' in request.session)
        self.assertEqual(request.session['oidc_login_next'], '/foo')

    @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='https://server.example.com/auth')
    @override_settings(OIDC_RP_CLIENT_ID='example_id')
    def test_missing_next_url(self):
        """Test that `next` url gets invalidated in user session."""
        url = reverse('oidc_authentication_init')
        request = self.factory.get(url)
        request.session = {
            'oidc_login_next': 'foobar'
        }
        login_view = views.OIDCAuthenticationRequestView.as_view()
        login_view(request)
        self.assertTrue('oidc_login_next' in request.session)
        self.assertTrue(request.session['oidc_login_next'] is None)


class OIDCLogoutViewTestCase(TestCase):
    def setUp(self):
        self.factory = RequestFactory()

    @override_settings(LOGOUT_REDIRECT_URL='/example-logout')
    def test_get_anonymous_user(self):
        url = reverse('oidc_logout')
        request = self.factory.post(url)
        request.user = AnonymousUser()
        logout_view = views.OIDCLogoutView.as_view()

        response = logout_view(request)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, '/example-logout')

    @override_settings(LOGOUT_REDIRECT_URL='/example-logout')
    def test_post(self):
        user = User.objects.create_user('example_username')
        url = reverse('oidc_logout')
        request = self.factory.post(url)
        request.user = user
        logout_view = views.OIDCLogoutView.as_view()

        with patch('mozilla_django_oidc.views.auth.logout') as mock_logout:
            response = logout_view(request)
            mock_logout.assert_called_once_with(request)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, '/example-logout')

    @override_settings(LOGOUT_REDIRECT_URL='/example-logout')
    @override_settings(OIDC_OP_LOGOUT_URL_METHOD='tests.test_views.my_custom_op_logout')
    def test_post_with_OIDC_OP_LOGOUT_URL_METHOD(self):
        user = User.objects.create_user('example_username')
        url = reverse('oidc_logout')
        request = self.factory.post(url)
        request.user = user
        logout_view = views.OIDCLogoutView.as_view()

        with patch('mozilla_django_oidc.views.auth.logout') as mock_logout:
            response = logout_view(request)
            mock_logout.assert_called_once_with(request)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, 'http://testserver/logged/out')