from urllib.parse import urlencode

import pytest
from django.conf import settings
from django.http import HttpResponse
from django.test.client import Client
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from httpretty import httpretty
from social_core.backends.utils import get_backend

from auth_backends.helsinki_tunnistus_suomifi import HelsinkiTunnistus
from tunnistamo.tests.conftest import DummyFixedOidcBackend, reload_social_django_utils
from users.models import LoginMethod


class CancelExampleComRedirectClient(Client):
    def get(self, path, data=None, follow=False, secure=False, **extra):
        # If the request is to a remote example.com address just return an empty response
        # without really making the request
        if 'example.com' in extra.get('SERVER_NAME', ''):
            return HttpResponse()

        return super().get(path, data=data, follow=follow, secure=secure, **extra)


def _start_oidc_authorize(django_client, oidcclient_factory, backend_name=DummyFixedOidcBackend.name, state=None):
    """Start OIDC authorization flow

    The client will be redirected to the Tunnistamo login view and from there to the
    "Test login method". The redirects are required to have the "next" parameter in the
    django_clients session."""
    LoginMethod.objects.create(
        provider_id=backend_name,
        name='Test login method',
        order=1,
    )

    redirect_uris = ['https://example.com/callback']
    oidc_client = oidcclient_factory(redirect_uris=redirect_uris)

    authorize_url = reverse('authorize')
    authorize_data = {
        'client_id': oidc_client.client_id,
        'response_type': 'id_token token',
        'redirect_uri': redirect_uris[0],
        'scope': 'openid',
        'response_mode': 'form_post',
        'nonce': 'abcdefg',
    }
    if state:
        authorize_data['state'] = state

    backend = get_backend(settings.AUTHENTICATION_BACKENDS, backend_name)
    backend_oidc_config_url = backend().setting('OIDC_ENDPOINT') + '/.well-known/openid-configuration'
    backend_authorize_url = backend().setting('OIDC_ENDPOINT') + '/authorize'

    # Mock the open id connect configuration url so that the open id connect social auth
    # backend can generate the authorization url without calling the external server.
    httpretty.register_uri(
        httpretty.GET,
        backend_oidc_config_url,
        body='''
        {{
            "authorization_endpoint": "{}"
        }}
        '''.format(backend_authorize_url)
    )

    httpretty.enable()
    django_client.get(authorize_url, authorize_data, follow=True)
    httpretty.disable()

    return oidc_client


def _request_social_auth_complete_with_error(
    django_client,
    backend_name=DummyFixedOidcBackend.name,
    error='interaction_required'
):
    complete_url = reverse('social:complete', kwargs={
        'backend': backend_name,
    })
    complete_data = {
        'error': error,
        'error_description': 'Test error',
    }
    return django_client.get(complete_url, data=complete_data, follow=False)


@pytest.mark.django_db
@pytest.mark.parametrize('original_error,expected_error,expected_error_description', [
    ('unknown error code', 'interaction_required', _('Authentication failed')),
    ('interaction_required', 'interaction_required', _('Authentication failed')),
    ('access_denied', 'access_denied', _('Authentication cancelled or failed')),
])
def test_redirect_to_client_after_social_auth_error(
    settings,
    oidcclient_factory,
    original_error,
    expected_error,
    expected_error_description
):
    settings.SOCIAL_AUTH_HELTUNNISTUSSUOMIFI_OIDC_ENDPOINT = 'https://heltunnistussuomifi.example.com'
    django_client = CancelExampleComRedirectClient(backend_name=HelsinkiTunnistus.name)

    # Start the OIDC flow.
    state = get_random_string()
    oidc_client = _start_oidc_authorize(
        django_client,
        oidcclient_factory,
        backend_name=HelsinkiTunnistus.name,
        state=state,
    )

    # Return the user from the social auth with an error.
    response = _request_social_auth_complete_with_error(
        django_client,
        backend_name=HelsinkiTunnistus.name,
        error=original_error,
    )

    # Verify that the oidc provider would redirect the user back to the client
    expected_redirect_url = '{}?{}'.format(oidc_client.redirect_uris[0], urlencode({
        'error': expected_error,
        'error_description': expected_error_description,
        'state': state,
    }))
    assert response.status_code == 302
    assert response.url == expected_redirect_url


@pytest.mark.django_db
@pytest.mark.parametrize('on_error_redirect', (True, False))
def test_on_auth_error_redirect_to_client_setting(
    settings,
    oidcclient_factory,
    on_error_redirect,
):
    settings.AUTHENTICATION_BACKENDS = settings.AUTHENTICATION_BACKENDS + (
        'tunnistamo.tests.conftest.DummyFixedOidcBackend',
    )
    settings.SOCIAL_AUTH_DUMMYFIXEDOIDCBACKEND_OIDC_ENDPOINT = 'https://dummy.example.com'
    settings.SOCIAL_AUTH_DUMMYFIXEDOIDCBACKEND_ON_AUTH_ERROR_REDIRECT_TO_CLIENT = on_error_redirect
    reload_social_django_utils()

    django_client = CancelExampleComRedirectClient()

    # Start the OIDC flow.
    oidc_client = _start_oidc_authorize(django_client, oidcclient_factory)

    # Return the user from the social auth with an error.
    response = _request_social_auth_complete_with_error(django_client)

    assert response.status_code == 302

    if on_error_redirect:
        # Verify that the oidc provider would redirect the user back to the client
        expected_redirect_url = '{}?{}'.format(oidc_client.redirect_uris[0], urlencode({
            'error': 'interaction_required',
            'error_description': _('Authentication failed'),
        }))
        assert response.url == expected_redirect_url
    else:
        # Without the on error auth redirect setting the user should be redirected to
        # the login view
        assert response.url.startswith('/login/')


@pytest.mark.django_db
@pytest.mark.parametrize('on_error_redirect', (True, False))
def test_should_not_redirect_to_oidc_client_if_the_next_parameter_is_not_to_an_oidc_endpoint(
    settings,
    on_error_redirect,
):
    settings.AUTHENTICATION_BACKENDS = settings.AUTHENTICATION_BACKENDS + (
        'tunnistamo.tests.conftest.DummyFixedOidcBackend',
    )
    settings.SOCIAL_AUTH_DUMMYFIXEDOIDCBACKEND_OIDC_ENDPOINT = 'https://dummy.example.com'
    settings.SOCIAL_AUTH_DUMMYFIXEDOIDCBACKEND_ON_AUTH_ERROR_REDIRECT_TO_CLIENT = on_error_redirect
    reload_social_django_utils()

    django_client = CancelExampleComRedirectClient()
    session = django_client.session
    session['next'] = '/'
    session.save()

    # Return the user from the social auth with an error.
    response = _request_social_auth_complete_with_error(django_client)

    # Should redirect to login
    assert response.status_code == 302
    assert response.url.startswith('/login/')

from urllib.parse import urlencode

import pytest
from django.conf import settings
from django.http import HttpResponse
from django.test.client import Client
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from httpretty import httpretty
from social_core.backends.utils import get_backend

from auth_backends.helsinki_tunnistus_suomifi import HelsinkiTunnistus
from tunnistamo.tests.conftest import DummyFixedOidcBackend, reload_social_django_utils
from users.models import LoginMethod


class CancelExampleComRedirectClient(Client):
    def get(self, path, data=None, follow=False, secure=False, **extra):
        # If the request is to a remote example.com address just return an empty response
        # without really making the request
        if 'example.com' in extra.get('SERVER_NAME', ''):
            return HttpResponse()

        return super().get(path, data=data, follow=follow, secure=secure, **extra)


def _start_oidc_authorize(django_client, oidcclient_factory, backend_name=DummyFixedOidcBackend.name, state=None):
    """Start OIDC authorization flow

    The client will be redirected to the Tunnistamo login view and from there to the
    "Test login method". The redirects are required to have the "next" parameter in the
    django_clients session."""
    LoginMethod.objects.create(
        provider_id=backend_name,
        name='Test login method',
        order=1,
    )

    redirect_uris = ['https://example.com/callback']
    oidc_client = oidcclient_factory(redirect_uris=redirect_uris)

    authorize_url = reverse('authorize')
    authorize_data = {
        'client_id': oidc_client.client_id,
        'response_type': 'id_token token',
        'redirect_uri': redirect_uris[0],
        'scope': 'openid',
        'response_mode': 'form_post',
        'nonce': 'abcdefg',
    }
    if state:
        authorize_data['state'] = state

    backend = get_backend(settings.AUTHENTICATION_BACKENDS, backend_name)
    backend_oidc_config_url = backend().setting('OIDC_ENDPOINT') + '/.well-known/openid-configuration'
    backend_authorize_url = backend().setting('OIDC_ENDPOINT') + '/authorize'

    # Mock the open id connect configuration url so that the open id connect social auth
    # backend can generate the authorization url without calling the external server.
    httpretty.register_uri(
        httpretty.GET,
        backend_oidc_config_url,
        body='''
        {{
            "authorization_endpoint": "{}"
        }}
        '''.format(backend_authorize_url)
    )

    httpretty.enable()
    django_client.get(authorize_url, authorize_data, follow=True)
    httpretty.disable()

    return oidc_client


def _request_social_auth_complete_with_error(
    django_client,
    backend_name=DummyFixedOidcBackend.name,
    error='interaction_required'
):
    complete_url = reverse('social:complete', kwargs={
        'backend': backend_name,
    })
    complete_data = {
        'error': error,
        'error_description': 'Test error',
    }
    return django_client.get(complete_url, data=complete_data, follow=False)


@pytest.mark.django_db
@pytest.mark.parametrize('original_error,expected_error,expected_error_description', [
    ('unknown error code', 'interaction_required', _('Authentication failed')),
    ('interaction_required', 'interaction_required', _('Authentication failed')),
    ('access_denied', 'access_denied', _('Authentication cancelled or failed')),
])
def test_redirect_to_client_after_social_auth_error(
    settings,
    oidcclient_factory,
    original_error,
    expected_error,
    expected_error_description
):
    settings.SOCIAL_AUTH_HELTUNNISTUSSUOMIFI_OIDC_ENDPOINT = 'https://heltunnistussuomifi.example.com'
    django_client = CancelExampleComRedirectClient(backend_name=HelsinkiTunnistus.name)

    # Start the OIDC flow.
    state = get_random_string()
    oidc_client = _start_oidc_authorize(
        django_client,
        oidcclient_factory,
        backend_name=HelsinkiTunnistus.name,
        state=state,
    )

    # Return the user from the social auth with an error.
    response = _request_social_auth_complete_with_error(
        django_client,
        backend_name=HelsinkiTunnistus.name,
        error=original_error,
    )

    # Verify that the oidc provider would redirect the user back to the client
    expected_redirect_url = '{}?{}'.format(oidc_client.redirect_uris[0], urlencode({
        'error': expected_error,
        'error_description': expected_error_description,
        'state': state,
    }))
    assert response.status_code == 302
    assert response.url == expected_redirect_url


@pytest.mark.django_db
@pytest.mark.parametrize('on_error_redirect', (True, False))
def test_on_auth_error_redirect_to_client_setting(
    settings,
    oidcclient_factory,
    on_error_redirect,
):
    settings.AUTHENTICATION_BACKENDS = settings.AUTHENTICATION_BACKENDS + (
        'tunnistamo.tests.conftest.DummyFixedOidcBackend',
    )
    settings.SOCIAL_AUTH_DUMMYFIXEDOIDCBACKEND_OIDC_ENDPOINT = 'https://dummy.example.com'
    settings.SOCIAL_AUTH_DUMMYFIXEDOIDCBACKEND_ON_AUTH_ERROR_REDIRECT_TO_CLIENT = on_error_redirect
    reload_social_django_utils()

    django_client = CancelExampleComRedirectClient()

    # Start the OIDC flow.
    oidc_client = _start_oidc_authorize(django_client, oidcclient_factory)

    # Return the user from the social auth with an error.
    response = _request_social_auth_complete_with_error(django_client)

    assert response.status_code == 302

    if on_error_redirect:
        # Verify that the oidc provider would redirect the user back to the client
        expected_redirect_url = '{}?{}'.format(oidc_client.redirect_uris[0], urlencode({
            'error': 'interaction_required',
            'error_description': _('Authentication failed'),
        }))
        assert response.url == expected_redirect_url
    else:
        # Without the on error auth redirect setting the user should be redirected to
        # the login view
        assert response.url.startswith('/login/')


@pytest.mark.django_db
@pytest.mark.parametrize('on_error_redirect', (True, False))
def test_should_not_redirect_to_oidc_client_if_the_next_parameter_is_not_to_an_oidc_endpoint(
    settings,
    on_error_redirect,
):
    settings.AUTHENTICATION_BACKENDS = settings.AUTHENTICATION_BACKENDS + (
        'tunnistamo.tests.conftest.DummyFixedOidcBackend',
    )
    settings.SOCIAL_AUTH_DUMMYFIXEDOIDCBACKEND_OIDC_ENDPOINT = 'https://dummy.example.com'
    settings.SOCIAL_AUTH_DUMMYFIXEDOIDCBACKEND_ON_AUTH_ERROR_REDIRECT_TO_CLIENT = on_error_redirect
    reload_social_django_utils()

    django_client = CancelExampleComRedirectClient()
    session = django_client.session
    session['next'] = '/'
    session.save()

    # Return the user from the social auth with an error.
    response = _request_social_auth_complete_with_error(django_client)

    # Should redirect to login
    assert response.status_code == 302
    assert response.url.startswith('/login/')

# If the request is to a remote example.com address just return an empty response # without really making the request Start OIDC authorization flow      The client will be redirected to the Tunnistamo login view and from there to the     "Test login method". The redirects are required to have the "next" parameter in the     django_clients session. # Mock the open id connect configuration url so that the open id connect social auth # backend can generate the authorization url without calling the external server. {{             "authorization_endpoint": "{}"         }} # Start the OIDC flow. # Return the user from the social auth with an error. # Verify that the oidc provider would redirect the user back to the client # Start the OIDC flow. # Return the user from the social auth with an error. # Verify that the oidc provider would redirect the user back to the client # Without the on error auth redirect setting the user should be redirected to # the login view # Return the user from the social auth with an error. # Should redirect to login