# authentic2 - versatile identity manager
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import re

import lasso
import pytest
from django.contrib.auth import get_user_model
from mellon.models import UserSAMLIdentifier

from authentic2.custom_user.models import DeletedUser
from authentic2.models import Attribute
from authentic2_auth_saml.adapters import AuthenticAdapter, MappingError

from .utils import login

User = get_user_model()


def test_providers_on_login_page(db, app, settings):
    settings.A2_AUTH_SAML_ENABLE = True
    PROVIDERS = [
        {'METADATA': 'meta1.xml', 'ENTITY_ID': 'idp1'},
    ]
    settings.MELLON_IDENTITY_PROVIDERS = PROVIDERS
    response = app.get('/login/')
    assert response.pyquery('button[name="login-saml-0"]')
    assert not response.pyquery('button[name="login-saml-1"]')

    PROVIDERS = [
        {'METADATA': 'meta1.xml', 'ENTITY_ID': 'idp1', 'SLUG': 'idp1'},
    ]
    settings.MELLON_IDENTITY_PROVIDERS = PROVIDERS

    response = app.get('/login/')
    assert response.pyquery('button[name="login-saml-idp1"]')
    assert not response.pyquery('button[name="login-saml-1"]')

    PROVIDERS.append({'METADATA': 'meta1.xml', 'ENTITY_ID': 'idp1'})
    response = app.get('/login/')
    # two frontends should be present on login page
    assert response.pyquery('button[name="login-saml-idp1"]')
    assert response.pyquery('button[name="login-saml-1"]')


@pytest.fixture
def adapter():
    return AuthenticAdapter()


@pytest.fixture
def idp():

    return {
        'A2_ATTRIBUTE_MAPPING': [
            {
                'attribute': 'email',
                'saml_attribute': 'mail',
                'mandatory': True,
            },
            {'action': 'rename', 'from': 'http://fucking/attribute/givenName', 'to': 'first_name'},
            {
                'attribute': 'title',
                'saml_attribute': 'title',
            },
            {
                'attribute': 'first_name',
                'saml_attribute': 'first_name',
            },
        ]
    }


@pytest.fixture
def title_attribute(db):
    return Attribute.objects.create(kind='title', name='title', label='title')


@pytest.fixture
def saml_attributes():
    return {
        'issuer': 'https://idp.com/',
        'name_id_content': 'xxx',
        'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
        'mail': ['john.doe@example.com'],
        'title': ['Mr.'],
        'http://fucking/attribute/givenName': ['John'],
    }


@pytest.fixture
def user(db):
    return User.objects.create()


def test_lookup_user_ok(adapter, idp, saml_attributes, title_attribute):
    assert User.objects.count() == 0

    user = adapter.lookup_user(idp, saml_attributes)
    user.refresh_from_db()
    assert user.email == 'john.doe@example.com'
    assert user.attributes.title == 'Mr.'
    assert user.first_name == 'John'
    assert user.attributes.title == 'Mr.'
    assert user.ou.default is True


def test_lookup_user_missing_mandatory_attribute(adapter, idp, saml_attributes, title_attribute):
    del saml_attributes['mail']

    assert User.objects.count() == 0
    assert adapter.lookup_user(idp, saml_attributes) is None
    assert User.objects.count() == 0


def test_apply_attribute_mapping_missing_attribute_logged(
    caplog, adapter, idp, saml_attributes, title_attribute, user
):
    caplog.set_level('WARNING')
    saml_attributes['http://fucking/attribute/givenName'] = []
    adapter.apply_attribute_mapping(user, idp, saml_attributes, idp['A2_ATTRIBUTE_MAPPING'])
    assert re.match('.*no value.*first_name', caplog.records[-1].message)


def test_apply_attribute_mapping_missing_attribute_exception(
    adapter, idp, saml_attributes, title_attribute, user
):
    saml_attributes['http://fucking/attribute/givenName'] = []
    idp['A2_ATTRIBUTE_MAPPING'][-1]['mandatory'] = True
    with pytest.raises(MappingError, match='no value'):
        adapter.apply_attribute_mapping(user, idp, saml_attributes, idp['A2_ATTRIBUTE_MAPPING'])


@pytest.mark.parametrize('action_name', ['add-role', 'toggle-role'])
class TestAddRole:
    @pytest.fixture
    def idp(self, action_name, simple_role):
        return {
            'A2_ATTRIBUTE_MAPPING': [
                {
                    'action': action_name,
                    'role': {
                        'name': simple_role.name,
                        'ou': {
                            'name': simple_role.ou.name,
                        },
                    },
                    'condition': "roles == 'A'",
                }
            ]
        }

    @pytest.fixture
    def saml_attributes(self):
        return {
            'issuer': 'https://idp.com/',
            'name_id_content': 'xxx',
            'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
        }

    def test_lookup_user_condition_fails(self, adapter, simple_role, idp, saml_attributes):
        user = adapter.lookup_user(idp, saml_attributes)
        assert simple_role not in user.roles.all()

    def test_lookup_user_condition_success(self, adapter, simple_role, idp, saml_attributes):
        saml_attributes['roles'] = ['A']
        user = adapter.lookup_user(idp, saml_attributes)
        assert simple_role in user.roles.all()

    def test_lookup_user_mandatory_condition(self, adapter, simple_role, idp, saml_attributes):
        # if a toggle-role is mandatory, failure to evaluate condition block user creation
        idp['A2_ATTRIBUTE_MAPPING'][0]['mandatory'] = True
        assert adapter.lookup_user(idp, saml_attributes) is None

    def test_lookup_user_mandatory(self, adapter, simple_role, idp, saml_attributes):
        idp['A2_ATTRIBUTE_MAPPING'][0]['mandatory'] = True
        saml_attributes['roles'] = ['A']
        user = adapter.lookup_user(idp, saml_attributes)
        assert simple_role in user.roles.all()

    def test_lookup_user_use_list(self, adapter, simple_role, idp, saml_attributes):
        idp['A2_ATTRIBUTE_MAPPING'][0]['condition'] = "'A' in roles__list"
        saml_attributes['roles'] = ['A']
        user = adapter.lookup_user(idp, saml_attributes)
        assert simple_role in user.roles.all()

    def test_lookup_user_add_and_remove(self, adapter, simple_role, idp, saml_attributes, caplog):
        saml_attributes['roles'] = ['A']
        user = adapter.lookup_user(idp, saml_attributes)
        assert simple_role in user.roles.all()

        saml_attributes['roles'] = []
        adapter.provision(user, idp, saml_attributes)
        # condition failed, so role should be removed
        user.refresh_from_db()
        assert simple_role not in user.roles.all()


def test_login_with_conditionnal_authenticators(db, app, settings, caplog):

    settings.A2_AUTH_SAML_ENABLE = True
    settings.MELLON_IDENTITY_PROVIDERS = [
        {"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')}
    ]
    response = app.get('/login/')
    assert 'login-saml-0' in response

    settings.AUTH_FRONTENDS_KWARGS = {'saml': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
    response = app.get('/login/')
    assert 'login-saml-0' not in response

    settings.MELLON_IDENTITY_PROVIDERS.append(
        {"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')}
    )
    settings.AUTH_FRONTENDS_KWARGS = {'saml': {'show_condition': {'0': 'remote_addr==\'0.0.0.0\''}}}
    response = app.get('/login/')
    assert 'login-saml-0' not in response
    assert 'login-saml-1' in response

    settings.AUTH_FRONTENDS_KWARGS = {
        'saml': {'show_condition': {'0': 'remote_addr==\'0.0.0.0\'', '1': 'remote_addr==\'0.0.0.0\''}}
    }
    response = app.get('/login/')
    assert 'login-saml-0' not in response
    assert 'login-saml-1' not in response


def test_login_autorun(db, app, settings):
    response = app.get('/login/')

    settings.A2_AUTH_SAML_ENABLE = True
    settings.MELLON_IDENTITY_PROVIDERS = [
        {"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')}
    ]
    # hide password block
    settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
    response = app.get('/login/', status=302)
    assert '/accounts/saml/login/?entityID=' in response['Location']

    settings.MELLON_IDENTITY_PROVIDERS[0]['SLUG'] = 'slug_with_underscore'
    response = app.get('/login/', status=302)
    assert '/accounts/saml/login/?entityID=' in response['Location']


def test_save_account_on_delete_user(db):
    user = User.objects.create()
    UserSAMLIdentifier.objects.create(user=user, issuer='https://idp1.com/', name_id='1234')
    UserSAMLIdentifier.objects.create(user=user, issuer='https://idp2.com/', name_id='4567')

    user.delete()
    assert UserSAMLIdentifier.objects.count() == 0

    deleted_user = DeletedUser.objects.get()
    assert deleted_user.old_data.get('saml_accounts') == [
        {
            'issuer': 'https://idp1.com/',
            'name_id': '1234',
        },
        {
            'issuer': 'https://idp2.com/',
            'name_id': '4567',
        },
    ]


def test_manager_user_sidebar(app, superuser, simple_user):
    login(app, superuser, '/manage/')
    response = app.get('/manage/users/%s/' % simple_user.id)
    assert 'SAML' not in response

    UserSAMLIdentifier.objects.create(user=simple_user, issuer='https://idp1.com/', name_id='1234')

    response = app.get('/manage/users/%s/' % simple_user.id)
    assert 'SAML' in response
    assert 'https://idp1.com/' in response
    assert '1234' in response
