import datetime

import bs4
import flask
import freezegun
import pytest
from flask import url_for

from flask_saml2.exceptions import CannotHandleAssertion
from flask_saml2.utils import utcnow

from .base import SamlTestCase, User


class TestEndToEnd(SamlTestCase):
    """
    Test the SP and IdP as a user/browser, going through the whole login
    process, following the redirects, submitting the forms, etc.
    """
    def test_end_to_end(self):
        # Pretend we want to access this protected page
        login_next = 'http://sp.example.com/dashboard'

        with self.sp_app.app_context():
            # We go here to log in
            sp_login_url = url_for('flask_saml2_sp.login', next=login_next)
            response = self.sp_client.get(sp_login_url)

            # We should be redirected to the specific IdP login URL
            sp_login_idp_url = url_for(
                'flask_saml2_sp.login_idp',
                entity_id='http://idp.example.com/saml/metadata.xml',
                next=login_next, _external=True)
            assert response.status_code == 302
            assert response.headers['Location'] == sp_login_idp_url

            # Lets fetch that...
            response = self.sp_client.get(sp_login_idp_url)

        with self.idp_app.app_context():
            # Which should send us to the IdP
            idp_login_url = response.headers['Location']
            assert idp_login_url.startswith(
                url_for('flask_saml2_idp.login_begin', _external=True))

            # Which bounces us through the hoops
            response = self.idp_client.get(idp_login_url)
            assert response.status_code == 302
            assert response.headers['Location'] \
                == url_for('flask_saml2_idp.login_process', _external=True)

            process_url = response.headers['Location']
            response = self.idp_client.get(process_url)

            # Seems we need to log in!
            assert response.status_code == 302
            assert response.headers['Location'].startswith(self.idp.login_url)

            # Lets create a user and login as them
            user = User('alex', 'alex@example.com')
            self.login(user)

            # And try the process url again
            response = self.idp_client.get(process_url)
            assert response.status_code == 200

            # It returns an HTML form that gets POSTed to the SP
            doc = bs4.BeautifulSoup(response.data, 'html.parser')
            form = doc.find(id='logged_in_post_form')
            assert form.get('method') == 'post'

            # Collect the form details...
            target = form.get('action')
            inputs = form.find_all('input')
            data = {el.get('name'): el.get('value') for el in inputs if el.get('name')}

        with self.sp_app.app_context():
            # And hit the SP as if the form was posted
            assert target == url_for('flask_saml2_sp.acs', _external=True)
            response = self.sp_client.post(target, data=data)

            # This should send us onwards to the protected page
            assert response.status_code == 302
            assert response.headers['Location'] == login_next

            ctx = self.sp_app.test_request_context('/dashboard/', environ_base={
                'HTTP_COOKIE': response.headers['Set-Cookie']})
            with ctx:
                # We should also have been logged in, horray!
                auth_data = self.sp.get_auth_data_in_session()
                assert auth_data.nameid == user.email


class TestInvalidConditions(SamlTestCase):
    user = User('alex', 'alex@example.com')

    def _make_authn_request(self):
        # Make an AuthnRequest
        idp_handler = self.sp.get_idp_handler_by_entity_id('http://idp.example.com/saml/metadata.xml')
        with self.sp_app.app_context():
            authn_request = idp_handler.get_authn_request()
            return idp_handler.encode_saml_string(authn_request.get_xml_string())

    def _process_authn_request(self, authn_request):
        with self.idp_app.app_context():
            sp_handler = next(self.idp.get_sp_handlers())

            request_handler = sp_handler.parse_authn_request(authn_request)
            with self.idp_app.test_request_context('/saml/'):
                flask.session['user'] = 'alex'
                response_xml = sp_handler.make_response(request_handler)
                return sp_handler.encode_response(response_xml)

    def _process_authn_response(self, authn_response):
        idp_handler = self.sp.get_idp_handler_by_entity_id('http://idp.example.com/saml/metadata.xml')
        with self.sp_app.app_context():
            response_handler = idp_handler.get_response_parser(authn_response)
            return idp_handler.get_auth_data(response_handler)

    def test_too_early(self):
        now = utcnow()
        self.login(self.user)

        with freezegun.freeze_time(now) as frozen:
            authn_request = self._make_authn_request()

            # step forwards a bit for transmission time
            frozen.tick(delta=datetime.timedelta(seconds=30))

            authn_response = self._process_authn_request(authn_request)

            # step backwards a bunch
            frozen.tick(delta=datetime.timedelta(minutes=-5))

            with pytest.raises(CannotHandleAssertion, match='NotBefore'):
                self._process_authn_response(authn_response)

    def test_too_late(self):
        now = utcnow()
        self.login(self.user)

        with freezegun.freeze_time(now) as frozen:
            authn_request = self._make_authn_request()

            # step forwards a bit for transmission time
            frozen.tick(delta=datetime.timedelta(seconds=30))

            authn_response = self._process_authn_request(authn_request)

            # step backwards a bunch
            frozen.tick(delta=datetime.timedelta(minutes=25))

            with pytest.raises(CannotHandleAssertion, match='NotOnOrAfter'):
                self._process_authn_response(authn_response)

    def test_just_right(self):
        now = utcnow()
        self.login(self.user)

        with freezegun.freeze_time(now) as frozen:
            authn_request = self._make_authn_request()

            # step forwards a bit for transmission time
            frozen.tick(delta=datetime.timedelta(seconds=30))

            authn_response = self._process_authn_request(authn_request)

            # step forwards a bit for transmission times
            frozen.tick(delta=datetime.timedelta(seconds=30))

            auth_data = self._process_authn_response(authn_response)
            assert auth_data.nameid == self.user.email

    def test_bad_audience(self):
        self.login(self.user)

        authn_request = self._make_authn_request()
        authn_response = self._process_authn_request(authn_request)

        # Change the server name, which will change the EntityID, which
        # will cause a mismatch in the audience.
        self.sp_app.config['SERVER_NAME'] = 'sp.sample.net'

        with pytest.raises(CannotHandleAssertion, match='AudienceRestriction'):
            self._process_authn_response(authn_response)