# 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 pytest
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.management import call_command

from authentic2.a2_rbac.models import MANAGE_MEMBERS_OP
from authentic2.a2_rbac.models import OrganizationalUnit as OU
from authentic2.a2_rbac.models import Permission, Role, RoleAttribute
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.custom_user.models import User
from authentic2.models import Service
from authentic2.utils.misc import get_hex_uuid
from django_rbac.models import CHANGE_OP, Operation
from django_rbac.utils import get_permission_model

from .utils import login, request_select2


def test_update_rbac(db):
    # 3 content types managers and 1 global manager
    assert Role.objects.count() == 5
    # 3 content type global permissions, 1 role administration permissions (for the main manager
    # role which is self-administered)
    # and 1 user view permission (for the role administrator)
    # and 1 user manage authorizations permission (for the role administrator)
    # and 1 ou view permission (for the user and role administrators)
    assert Permission.objects.count() == 8


def test_delete_role(db):
    rcount = Role.objects.count()
    pcount = Permission.objects.count()
    new_role = Role.objects.create(name='Coucou')
    admin_role = new_role.get_admin_role()

    # There should two more roles, the role and its admin counterpart
    assert Role.objects.count() == rcount + 2

    # There should be two more permissions the manage-members permission on the role
    # and the manage-members permission on the admin role
    manage_members_perm = Permission.objects.by_target(new_role).get(operation__slug='manage_members')
    admin_role = Role.objects.get(
        admin_scope_ct=ContentType.objects.get_for_model(manage_members_perm),
        admin_scope_id=manage_members_perm.pk,
    )
    admin_manage_members_perm = Permission.objects.by_target(admin_role).get(operation__slug='manage_members')
    assert Permission.objects.count() == pcount + 2
    new_role.delete()
    with pytest.raises(Permission.DoesNotExist):
        Permission.objects.get(pk=manage_members_perm.pk)
    with pytest.raises(Role.DoesNotExist):
        Role.objects.get(pk=admin_role.pk)
    with pytest.raises(Permission.DoesNotExist):
        Permission.objects.get(pk=admin_manage_members_perm.pk)
    assert Role.objects.count() == rcount
    assert Permission.objects.count() == pcount


def test_access_control(db):
    role_ct = ContentType.objects.get_for_model(Role)
    role_admin_role = Role.objects.get_admin_role(role_ct, 'admin %s' % role_ct, 'admin-role')
    user1 = User.objects.create(username='john.doe')
    assert not user1.has_perm('a2_rbac.change_role')
    assert not user1.has_perm('a2_rbac.view_role')
    assert not user1.has_perm('a2_rbac.delete_role')
    assert not user1.has_perm('a2_rbac.add_role')
    role_admin_role.members.add(user1)
    del user1._rbac_perms_cache
    assert user1.has_perm('a2_rbac.change_role')
    assert user1.has_perm('a2_rbac.view_role')
    assert user1.has_perm('a2_rbac.delete_role')
    assert user1.has_perm('a2_rbac.add_role')


def test_admin_roles_startswith_a2(db):
    coin = Role.objects.create(name='Coin', slug='coin')
    coin.get_admin_role()
    for role in Role.objects.filter(admin_scope_ct__isnull=False):
        assert role.slug.startswith('_a2'), 'role %s slug must start with _a2: %s' % (role.name, role.slug)


def test_admin_roles_update_slug(db):
    user = User.objects.create(username='john.doe')
    name1 = 'Can manage john.doe'
    slug1 = 'can-manage-john-doe'
    admin_role1 = Role.objects.get_admin_role(user, name1, slug1, update_name=True, update_slug=True)
    assert admin_role1.name == name1
    assert admin_role1.slug == slug1
    name2 = 'Should manage john.doe'
    slug2 = 'should-manage-john-doe'
    admin_role2 = Role.objects.get_admin_role(user, name2, slug2, update_slug=True)
    assert admin_role2.name == name1
    assert admin_role2.slug == slug2
    admin_role3 = Role.objects.get_admin_role(user, name2, slug2, update_name=True)
    assert admin_role3.name == name2
    assert admin_role3.slug == slug2


def test_role_clean(db):
    coin = Role(name='Coin')
    coin.clean()
    coin.save()
    assert coin.slug == 'coin'
    with pytest.raises(ValidationError) as exc_info:
        Role(name='Coin2', slug='coin').full_clean()
    assert 'slug' in exc_info.value.error_dict
    with pytest.raises(ValidationError) as exc_info:
        Role(name='Coin', slug='coin2').full_clean()
    assert 'name' in exc_info.value.error_dict


def test_role_natural_key(db):
    ou = OU.objects.create(name='ou1', slug='ou1')
    s1 = Service.objects.create(name='s1', slug='s1')
    s2 = Service.objects.create(name='s2', slug='s2', ou=ou)
    r1 = Role.objects.create(name='r1', slug='r1')
    r2 = Role.objects.create(name='r2', slug='r2', ou=ou)
    r3 = Role.objects.create(name='r3', slug='r3', service=s1)
    r4 = Role.objects.create(name='r4', slug='r4', service=s2)

    for r in (r1, r2, r3, r4):
        assert Role.objects.get_by_natural_key(*r.natural_key()) == r
    assert r1.natural_key() == ['r1', None, None]
    assert r2.natural_key() == ['r2', ['ou1'], None]
    assert r3.natural_key() == ['r3', None, [None, 's1']]
    assert r4.natural_key() == ['r4', ['ou1'], [['ou1'], 's2']]
    ou.delete()
    with pytest.raises(Role.DoesNotExist):
        Role.objects.get_by_natural_key(*r2.natural_key())
    with pytest.raises(Role.DoesNotExist):
        Role.objects.get_by_natural_key(*r4.natural_key())


def test_basic_role_export_json(db):
    role = Role.objects.create(name='basic role', slug='basic-role', description='basic role description')
    role_dict = role.export_json()
    assert role_dict['name'] == role.name
    assert role_dict['slug'] == role.slug
    assert role_dict['uuid'] == role.uuid
    assert role_dict['description'] == role.description
    assert role_dict['external_id'] == role.external_id
    assert role_dict['ou'] is None
    assert role_dict['service'] is None


def test_role_with_ou_export_json(db):
    ou = OU.objects.create(name='ou', slug='ou')
    role = Role.objects.create(name='some role', ou=ou)
    role_dict = role.export_json()
    assert role_dict['ou'] == {'uuid': ou.uuid, 'slug': ou.slug, 'name': ou.name}


def test_role_with_service_export_json(db):
    service = Service.objects.create(name='service name', slug='service-name')
    role = Role.objects.create(name='some role', service=service)
    role_dict = role.export_json()
    assert role_dict['service'] == {'slug': service.slug, 'ou': None}


def test_role_with_service_with_ou_export_json(db):
    ou = OU.objects.create(name='ou', slug='ou')
    service = Service.objects.create(name='service name', slug='service-name', ou=ou)
    role = Role.objects.create(name='some role', service=service)
    role_dict = role.export_json()
    assert role_dict['service'] == {'slug': service.slug, 'ou': {'uuid': ou.uuid, 'slug': 'ou', 'name': 'ou'}}


def test_role_with_attributes_export_json(db):
    role = Role.objects.create(name='some role')
    attr1 = RoleAttribute.objects.create(role=role, name='attr1_name', kind='string', value='attr1_value')
    attr2 = RoleAttribute.objects.create(role=role, name='attr2_name', kind='string', value='attr2_value')

    role_dict = role.export_json(attributes=True)
    attributes = role_dict['attributes']
    assert len(attributes) == 2

    expected_attr_names = {attr1.name, attr2.name}
    for attr_dict in attributes:
        assert attr_dict['name'] in expected_attr_names
        expected_attr_names.remove(attr_dict['name'])
        target_attr = RoleAttribute.objects.filter(name=attr_dict['name']).first()
        assert attr_dict['kind'] == target_attr.kind
        assert attr_dict['value'] == target_attr.value


def test_role_with_parents_export_json(db):
    grand_parent_role = Role.objects.create(name='test grand parent role', slug='test-grand-parent-role')
    parent_1_role = Role.objects.create(name='test parent 1 role', slug='test-parent-1-role')
    parent_1_role.add_parent(grand_parent_role)
    parent_2_role = Role.objects.create(name='test parent 2 role', slug='test-parent-2-role')
    parent_2_role.add_parent(grand_parent_role)
    child_role = Role.objects.create(name='test child role', slug='test-child-role')
    child_role.add_parent(parent_1_role)
    child_role.add_parent(parent_2_role)

    child_role_dict = child_role.export_json(parents=True)
    assert child_role_dict['slug'] == child_role.slug
    parents = child_role_dict['parents']
    assert len(parents) == 2
    expected_slugs = {parent_1_role.slug, parent_2_role.slug}
    for parent in parents:
        assert parent['slug'] in expected_slugs
        expected_slugs.remove(parent['slug'])

    grand_parent_role_dict = grand_parent_role.export_json(parents=True)
    assert grand_parent_role_dict['slug'] == grand_parent_role.slug
    assert 'parents' not in grand_parent_role_dict

    parent_1_role_dict = parent_1_role.export_json(parents=True)
    assert parent_1_role_dict['slug'] == parent_1_role.slug
    parents = parent_1_role_dict['parents']
    assert len(parents) == 1
    assert parents[0]['slug'] == grand_parent_role.slug

    parent_2_role_dict = parent_2_role.export_json(parents=True)
    assert parent_2_role_dict['slug'] == parent_2_role.slug
    parents = parent_2_role_dict['parents']
    assert len(parents) == 1
    assert parents[0]['slug'] == grand_parent_role.slug


def test_role_with_permission_export_json(db):
    some_ou = OU.objects.create(name='some ou', slug='some-ou')
    role = Role.objects.create(name='role name', slug='role-slug')
    other_role = Role.objects.create(
        name='other role name', slug='other-role-slug', uuid=get_hex_uuid(), ou=some_ou
    )
    ou = OU.objects.create(name='basic ou', slug='basic-ou', description='basic ou description')
    Permission = get_permission_model()
    op = Operation.objects.get(slug='add')
    perm_saml = Permission.objects.create(
        operation=op,
        ou=ou,
        target_ct=ContentType.objects.get_for_model(ContentType),
        target_id=ContentType.objects.get(app_label="saml", model="libertyprovider").pk,
    )
    role.permissions.add(perm_saml)
    perm_role = Permission.objects.create(
        operation=op, ou=None, target_ct=ContentType.objects.get_for_model(Role), target_id=other_role.pk
    )
    role.permissions.add(perm_role)

    export = role.export_json(permissions=True)
    permissions = export['permissions']
    assert len(permissions) == 2
    assert permissions[0] == {
        'operation': {'slug': 'add'},
        'ou': {'uuid': ou.uuid, 'slug': ou.slug, 'name': ou.name},
        'target_ct': {'app_label': 'contenttypes', 'model': 'contenttype'},
        'target': {'model': 'libertyprovider', 'app_label': 'saml'},
    }
    assert permissions[1] == {
        'operation': {'slug': 'add'},
        'ou': None,
        'target_ct': {'app_label': 'a2_rbac', 'model': 'role'},
        'target': {
            'slug': 'other-role-slug',
            'service': None,
            'uuid': other_role.uuid,
            'ou': {'slug': 'some-ou', 'uuid': some_ou.uuid, 'name': 'some ou'},
            'name': 'other role name',
        },
    }


def test_ou_export_json(db):
    ou = OU.objects.create(
        name='basic ou',
        slug='basic-ou',
        description='basic ou description',
        username_is_unique=True,
        email_is_unique=True,
        default=False,
        validate_emails=True,
    )
    ou_dict = ou.export_json()
    assert ou_dict['name'] == ou.name
    assert ou_dict['slug'] == ou.slug
    assert ou_dict['uuid'] == ou.uuid
    assert ou_dict['description'] == ou.description
    assert ou_dict['username_is_unique'] == ou.username_is_unique
    assert ou_dict['email_is_unique'] == ou.email_is_unique
    assert ou_dict['default'] == ou.default
    assert ou_dict['validate_emails'] == ou.validate_emails


def test_admin_cleanup(db):
    r1 = Role.objects.create(name='r1')

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 0

    r1.get_admin_role()

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 1

    r1.delete()

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 0


def test_admin_cleanup_bulk_delete(db):
    r1 = Role.objects.create(name='r1')

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 0

    r1.get_admin_role()

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 1

    Role.objects.filter(name='r1').delete()

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 0


def test_admin_cleanup_failure(db):
    Role.objects.create(
        name='manager of r1',
        admin_scope_ct=ContentType.objects.get_for_model(Permission),
        admin_scope_id=9999,
    )

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 1

    Role.objects.cleanup()

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 0


def test_admin_cleanup_command(db):
    Role.objects.create(
        name='manager of r1',
        admin_scope_ct=ContentType.objects.get_for_model(Permission),
        admin_scope_id=9999,
    )

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 1

    call_command('cleanupauthentic')

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 0


def test_role_rename(db):
    r1 = Role.objects.create(name='r1')
    assert r1.slug == 'r1'

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 0

    assert not r1.get_admin_role(create=False)

    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 0

    ar1 = r1.get_admin_role()

    assert ar1
    assert ar1.name == 'Managers of role "r1"'
    assert ar1.slug == '_a2-managers-of-role-r1'
    assert Role.objects.filter(name__contains='r1', admin_scope_ct_id__isnull=False).count() == 1

    assert ar1.name == 'Managers of role "r1"'

    Role.objects.filter(pk=r1.pk).update(name='r1bis')

    r1.refresh_from_db()
    ar1.refresh_from_db()

    assert r1.name == 'r1bis'
    assert ar1.name == 'Managers of role "r1"'

    ar1 = r1.get_admin_role(create=False)

    assert ar1.name == 'Managers of role "r1bis"'
    assert ar1.slug == '_a2-managers-of-role-r1bis'

    r1.name = 'r1ter'
    r1.save()
    ar1.refresh_from_db()

    assert ar1.name == 'Managers of role "r1ter"'
    assert ar1.slug == '_a2-managers-of-role-r1ter'


def test_admin_role_user_view(db, settings, app, admin, simple_user, ou1, user_ou1, role_ou1):
    role_ou1.get_admin_role().members.add(simple_user)

    # Default: all users are visible
    response = login(app, simple_user, '/manage/roles/')
    response = response.click('role_ou1')
    select2_json = request_select2(app, response)
    assert select2_json['more'] is False
    assert {result['id'] for result in select2_json['results']} == {simple_user.id, user_ou1.id, admin.id}

    # with A2_RBAC_ROLE_ADMIN_RESTRICT_TO_OU_USERS after a reload of the admin
    # page, we should only see user from the same OU as the role
    settings.A2_RBAC_ROLE_ADMIN_RESTRICT_TO_OU_USERS = True
    role_ou1.get_admin_role()
    response = app.get('/manage/roles/')
    response = response.click('role_ou1')
    select2_json = request_select2(app, response)
    assert select2_json['more'] is False
    assert {result['id'] for result in select2_json['results']} == {user_ou1.id}


def test_no_managed_ct(transactional_db, settings):
    from django.core.management.sql import emit_post_migrate_signal

    call_command('flush', verbosity=0, interactive=False, database='default', reset_sequences=False)
    assert Role.objects.count() == 5
    OU.objects.create(name='OU1', slug='ou1')
    emit_post_migrate_signal(verbosity=0, interactive=False, db='default', created_models=[])
    assert Role.objects.count() == 5 + 4 + 4
    settings.A2_RBAC_MANAGED_CONTENT_TYPES = ()
    call_command('flush', verbosity=0, interactive=False, database='default', reset_sequences=False)
    assert Role.objects.count() == 0
    # create ou
    OU.objects.create(name='OU1', slug='ou1')
    emit_post_migrate_signal(verbosity=0, interactive=False, db='default', created_models=[])
    assert Role.objects.count() == 0


def test_global_manager_roles(db):
    manager = Role.objects.get(ou__isnull=True, slug='_a2-manager')
    ou_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-organizational-units')
    user_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-users')
    role_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-roles')
    service_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-services')
    assert ou_manager in manager.parents()
    assert user_manager in manager.parents()
    assert role_manager in manager.parents()
    assert service_manager in manager.parents()
    assert manager.parents(include_self=False).count() == 4
    assert Role.objects.count() == 5
    assert OU.objects.count() == 1


def test_manager_roles_multi_ou(db, ou1):
    manager = Role.objects.get(ou__isnull=True, slug='_a2-manager')
    ou_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-organizational-units')
    user_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-users')
    role_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-roles')
    service_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-services')
    assert ou_manager in manager.parents()
    assert user_manager in manager.parents()
    assert role_manager in manager.parents()
    assert service_manager in manager.parents()
    assert manager.parents(include_self=False).count() == 4

    for ou in [get_default_ou(), ou1]:
        manager = Role.objects.get(ou__isnull=True, slug=f'_a2-managers-of-{ou.slug}')
        user_manager = Role.objects.get(ou=ou, slug=f'_a2-manager-of-users-{ou.slug}')
        role_manager = Role.objects.get(ou=ou, slug=f'_a2-manager-of-roles-{ou.slug}')
        service_manager = Role.objects.get(ou=ou, slug=f'_a2-manager-of-services-{ou.slug}')

        assert user_manager in manager.parents()
        assert role_manager in manager.parents()
        assert service_manager in manager.parents()
        assert manager.parents(include_self=False).count() == 3

    # 5 global roles and 4 ou roles for both ous
    assert Role.objects.count() == 5 + 4 + 4


@pytest.mark.parametrize(
    'alert,deletion', [(-1, 31), (31, -1), (0, 31), (31, 0), (None, 31), (31, None), (32, 31)]
)
def test_unused_account_settings_validation(db, ou1, alert, deletion):
    ou1.clean_unused_accounts_alert = alert
    ou1.clean_unused_accounts_deletion = deletion
    with pytest.raises(ValidationError):
        ou1.full_clean()


def test_update_content_types_roles(transactional_db, simple_user):
    from django.core.management.sql import emit_post_migrate_signal

    role = Role.objects.get(name='Manager of users')

    # for the purpose of this test, remove admin perm that inherit manage_authorizations perm
    admin_perm = [x for x in role.permissions.all() if x.operation.name == 'Management'][0]
    role.permissions.remove(admin_perm)

    # 'Manager of users' role gives manage_authorizations perm
    manage_authorizations_perm = [
        x for x in role.permissions.all() if x.operation.name == 'Manage service consents'
    ][0]
    assert manage_authorizations_perm
    simple_user.roles.add(role)
    assert simple_user.has_perm('custom_user.manage_authorizations_user')

    def update_user_permissions():
        del simple_user._rbac_perms_cache
        simple_user.roles.add(role)

    # remove manage_authorizations perm
    manage_authorizations_perm.delete()
    assert not [x for x in role.permissions.all() if x.operation.name == 'Manage service consents']
    update_user_permissions()
    assert not simple_user.has_perm('custom_user.manage_authorizations_user')
    assert not [x for x in simple_user.get_all_permissions() if x == 'custom_user.manage_authorizations_user']

    # manage_authorizations perm is (re-)created on post migrate
    emit_post_migrate_signal(verbosity=0, interactive=False, db='default', created_models=[])
    assert [x for x in role.permissions.all() if x.operation.name == 'Manage service consents'][0]
    # and added to 'Manager of users' role
    update_user_permissions()
    assert simple_user.has_perm('custom_user.manage_authorizations_user')
    assert [x for x in simple_user.get_all_permissions() if x == 'custom_user.manage_authorizations_user']


@pytest.mark.parametrize('new_perm_exists', [True, False])
def test_update_self_admin_perm_migration(migration, new_perm_exists):
    old_apps = migration.before([('a2_rbac', '0022_auto_20200402_1101')])
    Role = old_apps.get_model('a2_rbac', 'Role')
    OrganizationalUnit = old_apps.get_model('a2_rbac', 'OrganizationalUnit')
    Permission = old_apps.get_model('a2_rbac', 'Permission')
    Operation = old_apps.get_model('django_rbac', 'Operation')
    ContentType = old_apps.get_model('contenttypes', 'ContentType')
    ct = ContentType.objects.get_for_model(Role)
    change_op, _ = Operation.objects.get_or_create(slug=CHANGE_OP.slug)
    manage_members_op, _ = Operation.objects.get_or_create(slug=MANAGE_MEMBERS_OP.slug)

    # add old self administration
    role = Role.objects.create(name='name', slug='slug')
    self_perm, _ = Permission.objects.get_or_create(operation=change_op, target_ct=ct, target_id=role.pk)
    role.permissions.add(self_perm)

    if new_perm_exists:
        new_self_perm, _ = Permission.objects.get_or_create(
            operation=manage_members_op, target_ct=ct, target_id=role.pk
        )
    else:
        Permission.objects.filter(operation=manage_members_op, target_ct=ct, target_id=role.pk).delete()

    new_apps = migration.apply([('a2_rbac', '0024_fix_self_admin_perm')])
    Role = new_apps.get_model('a2_rbac', 'Role')
    Operation = old_apps.get_model('django_rbac', 'Operation')

    role = Role.objects.get(slug='slug')
    assert role.permissions.count() == 1

    perm = role.permissions.first()
    assert perm.operation.pk == manage_members_op.pk
    assert perm.target_ct.pk == ct.pk
    assert perm.target_id == role.pk

    if new_perm_exists:
        assert perm.pk == new_self_perm.pk

    assert not Permission.objects.filter(operation=change_op, target_ct=ct, target_id=role.pk).exists()
