# 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 datetime
import importlib
import json
from io import BufferedReader, BufferedWriter, TextIOWrapper

import py
import pytest
import responses
import webtest
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.timezone import now
from mellon.models import Issuer, UserSAMLIdentifier

from authentic2.a2_rbac.models import (
    ADMIN_OP,
    MANAGE_MEMBERS_OP,
    VIEW_OP,
    Operation,
    OrganizationalUnit,
    Permission,
    Role,
)
from authentic2.a2_rbac.utils import get_default_ou, get_operation
from authentic2.apps.journal.models import Event
from authentic2.custom_user.models import DeletedUser
from authentic2.models import UserExternalId
from authentic2_auth_oidc.models import OIDCAccount, OIDCProvider

from .utils import call_command, login

User = get_user_model()


def test_changepassword(db, simple_user, monkeypatch):
    import getpass

    def _getpass(*args, **kwargs):
        return 'pass'

    monkeypatch.setattr(getpass, 'getpass', _getpass)
    call_command('changepassword', 'user')
    old_pass = simple_user.password
    simple_user.refresh_from_db()
    assert old_pass != simple_user.password


def test_clean_unused_account(db, simple_user, mailoutbox, freezer, settings):
    settings.LDAP_AUTH_SETTINGS = [{'realm': 'ldap', 'url': 'ldap://ldap.com/', 'basedn': 'dc=ldap,dc=com'}]
    ldap_user = User.objects.create(username='ldap-user', email='ldap-user@example.com', ou=simple_user.ou)
    oidc_user = User.objects.create(username='oidc-user', email='oidc-user@example.com', ou=simple_user.ou)
    saml_user = User.objects.create(username='saml-user', email='saml-user@example.com', ou=simple_user.ou)
    UserExternalId.objects.create(user=ldap_user, source='ldap', external_id='whatever')
    provider = OIDCProvider.objects.create(name='oidc', ou=simple_user.ou)
    OIDCAccount.objects.create(user=oidc_user, provider=provider, sub='1')

    issuer = Issuer.objects.create(entity_id='https://idp1.example.com/', slug='idp1')
    UserSAMLIdentifier.objects.create(user=saml_user, issuer=issuer, name_id='1234')

    email = simple_user.email
    freezer.move_to('2018-01-01')
    simple_user.ou.clean_unused_accounts_alert = 2
    simple_user.ou.clean_unused_accounts_deletion = 3
    simple_user.ou.save()

    last_login = now() - datetime.timedelta(days=2, seconds=30)
    for user in (simple_user, ldap_user, oidc_user, saml_user):
        user.last_login = last_login
        user.save()

    call_command('clean-unused-accounts')

    assert User.objects.count() == 4
    assert len(mailoutbox) == 1
    assert (
        Event.objects.filter(
            type__name='user.notification.inactivity', user=simple_user, data__identifier=simple_user.email
        ).count()
        == 1
    )

    freezer.move_to('2018-01-01 12:00:00')
    # no new mail, no deletion
    call_command('clean-unused-accounts')
    assert User.objects.count() == 4
    assert len(mailoutbox) == 1

    freezer.move_to('2018-01-02')
    call_command('clean-unused-accounts')
    assert User.objects.count() == 3
    deleted_user = DeletedUser.objects.get()
    assert deleted_user.old_user_id == simple_user.id
    assert len(mailoutbox) == 2
    assert mailoutbox[-1].to == [email]
    assert (
        Event.objects.filter(
            type__name='user.deletion.inactivity', user=simple_user, data__identifier=simple_user.email
        ).count()
        == 1
    )


@responses.activate
def test_clean_unused_account_sms(db, nomail_user, mailoutbox, freezer, settings, phone_activated_authn):
    settings.LDAP_AUTH_SETTINGS = [{'realm': 'ldap', 'url': 'ldap://ldap.com/', 'basedn': 'dc=ldap,dc=com'}]
    settings.SMS_URL = 'https://foo.whatever.none/'
    rsps = responses.post(
        settings.SMS_URL,
        json={
            'headers': {
                'content-type': 'application/json',
            },
            'status_code': 200,
            'data': {},
        },
    )
    ldap_user = User.objects.create(username='ldap-user', ou=nomail_user.ou)
    ldap_user.attributes.phone = '+33122334455'
    ldap_user.save()
    oidc_user = User.objects.create(username='oidc-user', ou=nomail_user.ou)
    oidc_user.attributes.phone = '+33122334456'
    oidc_user.save()
    saml_user = User.objects.create(username='saml-user', ou=nomail_user.ou)
    saml_user.attributes.phone = '+33122334457'
    saml_user.save()
    UserExternalId.objects.create(user=ldap_user, source='ldap', external_id='whatever')
    provider = OIDCProvider.objects.create(name='oidc', ou=nomail_user.ou)
    OIDCAccount.objects.create(user=oidc_user, provider=provider, sub='1')

    issuer = Issuer.objects.create(entity_id='https://idp1.example.com/', slug='idp1')
    UserSAMLIdentifier.objects.create(user=saml_user, issuer=issuer, name_id='1234')

    freezer.move_to('2018-01-01')
    nomail_user.attributes.phone = '+33611223344'
    nomail_user.save()
    nomail_user.ou.clean_unused_accounts_alert = 2
    nomail_user.ou.clean_unused_accounts_deletion = 3
    nomail_user.ou.save()

    last_login = now() - datetime.timedelta(days=2, seconds=30)
    for user in (nomail_user, ldap_user, oidc_user, saml_user):
        user.last_login = last_login
        user.save()

    call_command('clean-unused-accounts')
    assert rsps.call_count == 1
    assert 'Your account is inactive, please log in' in json.loads(rsps.calls[-1].request.body)['message']
    # check message contains login url
    assert 'https://testserver/login/' in json.loads(rsps.calls[-1].request.body)['message']

    assert User.objects.count() == 4
    assert len(mailoutbox) == 0
    assert (
        Event.objects.filter(
            type__name='user.notification.inactivity',
            user=nomail_user,
            data__identifier=nomail_user.attributes.phone,
        ).count()
        == 1
    )

    freezer.move_to('2018-01-01 12:00:00')
    # no new sms, no deletion
    call_command('clean-unused-accounts')
    assert rsps.call_count == 1

    assert User.objects.count() == 4
    assert len(mailoutbox) == 0

    freezer.move_to('2018-01-02')
    call_command('clean-unused-accounts')
    assert rsps.call_count == 2
    assert (
        'Your account was inactive and has therefore been deleted.'
        in json.loads(rsps.calls[-1].request.body)['message']
    )

    assert User.objects.count() == 3
    deleted_user = DeletedUser.objects.get()
    assert deleted_user.old_user_id == nomail_user.id
    assert (
        Event.objects.filter(
            type__name='user.deletion.inactivity',
            user=nomail_user,
            data__identifier=nomail_user.attributes.phone,
        ).count()
        == 1
    )


def test_clean_unused_account_user_logs_in(app, db, simple_user, mailoutbox, freezer):
    freezer.move_to('2018-01-01')
    simple_user.ou.clean_unused_accounts_alert = 2
    simple_user.ou.clean_unused_accounts_deletion = 3
    simple_user.ou.save()

    simple_user.last_login = now() - datetime.timedelta(days=2)
    simple_user.save()

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 1

    login(app, simple_user)

    # the day of deletion, nothing happens
    freezer.move_to('2018-01-02')
    simple_user.refresh_from_db()
    assert len(mailoutbox) == 1

    # when new alert delay is reached, user gets alerted again
    freezer.move_to('2018-01-04')
    call_command('clean-unused-accounts')
    simple_user.refresh_from_db()
    assert len(mailoutbox) == 2


def test_clean_unused_account_keepalive(app, db, simple_user, mailoutbox, freezer):
    freezer.move_to('2018-01-01')
    simple_user.ou.clean_unused_accounts_alert = 20
    simple_user.ou.clean_unused_accounts_deletion = 30
    simple_user.ou.save()

    simple_user.last_login = None
    simple_user.date_joined = datetime.date.fromisoformat('2015-01-01')
    simple_user.keepalive = datetime.date.fromisoformat('2018-01-01')
    simple_user.save()

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 0

    freezer.move_to('2018-01-22')

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 1

    freezer.move_to('2018-02-23')
    # new command execution is past the theoretical deletion date, however
    # a new keepalive has been pushed in the meantime, the user should not
    # be deleted
    simple_user.keepalive = datetime.date.fromisoformat('2018-02-10')
    simple_user.save()

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 1
    assert DeletedUser.objects.count() == 0

    freezer.move_to('2018-03-22')
    call_command('clean-unused-accounts')
    # new alert
    assert len(mailoutbox) == 2
    assert DeletedUser.objects.count() == 0

    freezer.move_to('2018-04-22')
    call_command('clean-unused-accounts')
    # deletion notification
    assert len(mailoutbox) == 3
    assert DeletedUser.objects.get().old_user_id == simple_user.id


def test_clean_unused_account_keepalive_alert_inconsistency_failsafe(
    app, db, simple_user, mailoutbox, freezer
):
    freezer.move_to('2018-01-01')
    simple_user.ou.clean_unused_accounts_alert = 20
    simple_user.ou.clean_unused_accounts_deletion = 30
    simple_user.ou.save()

    simple_user.last_login = None
    simple_user.date_joined = datetime.date.fromisoformat('2015-01-01')
    simple_user.keepalive = datetime.date.fromisoformat('2018-01-01')
    simple_user.save()

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 0

    freezer.move_to('2018-01-22')

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 1

    freezer.move_to('2018-02-23')
    # oops, something went wrong, last account deletion alert has not been saved right or has been
    # erroneously modified to a more recent timestamp
    simple_user.last_account_deletion_alert = datetime.date.fromisoformat('2018-02-04')
    # and keepalive has been righteously updated in between
    simple_user.keepalive = datetime.date.fromisoformat('2018-02-03')
    simple_user.save()

    call_command('clean-unused-accounts')
    # no redundant alert, no deletion, i.e. the most restrictive choice in case of such
    # inconsistency
    assert len(mailoutbox) == 1
    assert DeletedUser.objects.count() == 0

    freezer.move_to('2018-02-04')
    # another way things can go wrong: the last alert is right but the keepalive has been updated
    # to an anterior (yet more recent than first execution) date
    simple_user.last_account_deletion_alert = datetime.date.fromisoformat('2018-01-22')
    simple_user.keepalive = datetime.date.fromisoformat('2018-01-21')
    simple_user.save()

    call_command('clean-unused-accounts')
    # no redundant alert, no deletion, i.e. the most restrictive choice also
    assert len(mailoutbox) == 1
    assert DeletedUser.objects.count() == 0


def test_clean_unused_account_never_logged_in(app, db, simple_user, mailoutbox, freezer):
    freezer.move_to('2018-01-01')
    simple_user.ou.clean_unused_accounts_alert = 2
    simple_user.ou.clean_unused_accounts_deletion = 3
    simple_user.ou.save()

    simple_user.last_login = None
    simple_user.keepalive = None
    simple_user.date_joined = now() - datetime.timedelta(days=4)
    simple_user.save()

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 1

    freezer.move_to('2018-01-03')
    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 2
    assert (
        Event.objects.filter(
            type__name='user.deletion.inactivity', user=simple_user, data__identifier=simple_user.email
        ).count()
        == 1
    )
    deleted_user = DeletedUser.objects.get()
    assert deleted_user.old_user_id == simple_user.id


def test_clean_unused_federated_account_never_logged_in(app, db, simple_user, mailoutbox, freezer, settings):
    freezer.move_to('2018-01-01')
    settings.LDAP_AUTH_SETTINGS = [{'realm': 'ldap', 'url': 'ldap://ldap.com/', 'basedn': 'dc=ldap,dc=com'}]
    ldap_user = User.objects.create(username='ldap-user', email='ldap-user@example.com', ou=simple_user.ou)
    UserExternalId.objects.create(user=ldap_user, source='ldap', external_id='whatever')

    simple_user.ou.clean_unused_accounts_alert = 2
    simple_user.ou.clean_unused_accounts_deletion = 3
    simple_user.ou.save()

    simple_user.last_login = simple_user.keepalive = None
    simple_user.date_joined = now() - datetime.timedelta(days=4)
    simple_user.save()

    oidc_provider = OIDCProvider.objects.create(name='Foo', slug='foo', enabled=True)
    OIDCAccount.objects.create(
        provider=oidc_provider,
        user=simple_user,
        sub='abc',
    )

    ldap_user.last_login = ldap_user.keepalive = None
    ldap_user.date_joined = now() - datetime.timedelta(days=4)
    ldap_user.save()

    saml_user = User.objects.create(username='saml-user', email='saml_user@example.com', ou=simple_user.ou)
    saml_issuer = Issuer.objects.create(entity_id='https://idp1.example.com/', slug='idp1')
    UserSAMLIdentifier.objects.create(
        user=saml_user,
        issuer=saml_issuer,
        name_id='1234',
    )

    saml_user.last_login = saml_user.keepalive = None
    saml_user.date_joined = now() - datetime.timedelta(days=4)
    saml_user.save()

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 0

    freezer.move_to('2018-01-03')
    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 0
    assert (
        Event.objects.filter(
            type__name='user.deletion.inactivity', user=simple_user, data__identifier=simple_user.email
        ).count()
        == 1
    )
    assert (
        Event.objects.filter(
            type__name='user.deletion.inactivity', user=ldap_user, data__identifier=ldap_user.email
        ).count()
        == 0
    )
    assert (
        Event.objects.filter(
            type__name='user.deletion.inactivity', user=saml_user, data__identifier=saml_user.email
        ).count()
        == 1
    )
    assert DeletedUser.objects.count() == 2
    assert {deleted.old_user_id for deleted in DeletedUser.objects.all()} == {saml_user.id, simple_user.id}

    ldap_user.last_login = ldap_user.keepalive = now() - datetime.timedelta(days=4)
    ldap_user.date_joined = now() - datetime.timedelta(days=5)
    ldap_user.save()

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 0
    assert (
        Event.objects.filter(
            type__name='user.deletion.inactivity', user=ldap_user, data__identifier=ldap_user.email
        ).count()
        == 0
    )
    assert DeletedUser.objects.count() == 2


def test_clean_unused_federated_account_logged_in_untouched(app, db, simple_user, mailoutbox, freezer):
    freezer.move_to('2018-01-01')
    simple_user.ou.clean_unused_accounts_alert = 2
    simple_user.ou.clean_unused_accounts_deletion = 3
    simple_user.ou.save()

    simple_user.last_login = simple_user.date_joined = now() - datetime.timedelta(days=4)
    simple_user.keepalive = None
    simple_user.save()

    provider = OIDCProvider.objects.create(name='Foo', slug='foo', enabled=True)
    OIDCAccount.objects.create(
        provider=provider,
        user=simple_user,
        sub='abc',
    )

    saml_user = User.objects.create(username='saml-user', email='saml_user@example.com', ou=simple_user.ou)
    saml_issuer = Issuer.objects.create(entity_id='https://idp1.example.com/', slug='idp1')
    UserSAMLIdentifier.objects.create(
        user=saml_user,
        issuer=saml_issuer,
        name_id='1234',
    )

    saml_user.last_login = simple_user.date_joined = now() - datetime.timedelta(days=4)
    saml_user.keepalive = None
    saml_user.save()

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 0

    freezer.move_to('2018-01-03')
    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 0
    assert (
        Event.objects.filter(
            type__name='user.deletion.inactivity', user=simple_user, data__identifier=simple_user.email
        ).count()
        == 0
    )
    assert (
        Event.objects.filter(
            type__name='user.deletion.inactivity', user=saml_user, data__identifier=saml_user.email
        ).count()
        == 0
    )
    assert not DeletedUser.objects.count()


def test_clean_unused_account_keepalive_after_alert(app, db, simple_user, mailoutbox, freezer):
    freezer.move_to('2018-01-01')
    simple_user.ou.clean_unused_accounts_alert = 2
    simple_user.ou.clean_unused_accounts_deletion = 3
    simple_user.ou.save()

    simple_user.last_login = now() - datetime.timedelta(days=2)
    simple_user.save()

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 1

    login(app, simple_user)

    # the day of deletion, nothing happens
    freezer.move_to('2018-01-02')
    assert len(mailoutbox) == 1

    freezer.move_to('2018-01-03')
    # set keepalive
    simple_user.keepalive = now()
    simple_user.save()

    # when new alert delay is reached, no mail is sent and last_account_deletion_alert is reset
    freezer.move_to('2018-01-04')
    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 1
    simple_user.refresh_from_db()
    assert simple_user.last_account_deletion_alert is None


def test_clean_unused_account_disabled_by_default(db, simple_user, mailoutbox):
    simple_user.last_login = now() - datetime.timedelta(days=2)
    simple_user.save()

    call_command('clean-unused-accounts')
    simple_user.refresh_from_db()
    assert len(mailoutbox) == 0


def test_clean_unused_account_a2_user_exclude(app, db, simple_user, mailoutbox, freezer, settings):
    settings.A2_USER_EXCLUDE = {'username': simple_user.username}
    freezer.move_to('2018-01-01')
    simple_user.ou.clean_unused_accounts_alert = 2
    simple_user.ou.clean_unused_accounts_deletion = 3
    simple_user.ou.save()

    simple_user.last_login = now() - datetime.timedelta(days=2)
    simple_user.save()

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 0


def test_clean_unused_account_always_alert(db, simple_user, mailoutbox, freezer):
    simple_user.ou.clean_unused_accounts_alert = 2
    simple_user.ou.clean_unused_accounts_deletion = 3  # one day between alert and actual deletion
    simple_user.ou.save()

    simple_user.last_login = now() - datetime.timedelta(days=4)
    simple_user.save()

    # even if account last login in past deletion delay, an alert is always sent first
    call_command('clean-unused-accounts')
    simple_user.refresh_from_db()
    assert len(mailoutbox) == 1

    # and calling again as no effect, since one day must pass before account is deleted
    call_command('clean-unused-accounts')
    simple_user.refresh_from_db()
    assert len(mailoutbox) == 1


@pytest.mark.parametrize('deletion_delay', [730, 500, 65])
def test_clean_unused_account_displayed_message(simple_user, mailoutbox, deletion_delay):
    simple_user.ou.clean_unused_accounts_alert = deletion_delay - 30
    simple_user.ou.clean_unused_accounts_deletion = deletion_delay
    simple_user.ou.save()
    simple_user.last_login = now() - datetime.timedelta(days=deletion_delay + 30)
    simple_user.save()

    # alert email
    call_command('clean-unused-accounts')
    mail = mailoutbox[0]
    assert mail.subject == 'Alert: Jôhn Dôe, your account is inactive and is pending deletion'
    assert 'Jôhn Dôe' in mail.body
    assert 'In order to keep your account, you must log in within 30 days.' in mail.body

    # deletion email
    simple_user.last_account_deletion_alert = now() - datetime.timedelta(days=31)
    simple_user.save()
    call_command('clean-unused-accounts')
    mail = mailoutbox[1]
    assert mail.subject == 'Notification: Jôhn Dôe, your account has been deleted'
    assert 'Jôhn Dôe' in mail.body
    assert 'Your account was inactive, it has been deleted.' in mail.body


def test_clean_unused_account_login_url(simple_user, mailoutbox):
    simple_user.ou.clean_unused_accounts_alert = 1
    simple_user.ou.clean_unused_accounts_deletion = 2
    simple_user.ou.save()
    simple_user.last_login = now() - datetime.timedelta(days=1)
    simple_user.save()
    call_command('clean-unused-accounts')
    mail = mailoutbox[0]
    assert 'href="https://testserver/login/"' in mail.message().as_string()


def test_clean_unused_account_with_no_email(simple_user, mailoutbox, caplog):
    simple_user.email = ''
    simple_user.ou.clean_unused_accounts_alert = 1
    simple_user.ou.clean_unused_accounts_deletion = 2
    simple_user.ou.save()
    simple_user.last_login = now() - datetime.timedelta(days=1)
    simple_user.save()
    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 0
    assert 'clean-unused-accounts failed' not in caplog.text


def test_cleanupauthentic(db):
    call_command('cleanupauthentic')


def test_load_ldif(db, monkeypatch, tmpdir):
    FileType = (TextIOWrapper, BufferedReader, BufferedWriter)
    ldif = tmpdir.join('some.ldif')
    ldif.ensure()

    class MockPArser:
        def __init__(self, *args, **kwargs):
            self.users = []
            assert len(args) == 1
            assert isinstance(args[0], FileType)
            assert kwargs['options']['extra_attribute'] == {'ldap_attr': 'first_name'}
            assert kwargs['options']['result'] == 'result'

        def parse(self):
            pass

    oidc_cmd = importlib.import_module('authentic2.management.commands.load-ldif')
    monkeypatch.setattr(oidc_cmd, 'DjangoUserLDIFParser', MockPArser)
    call_command('load-ldif', ldif.strpath, result='result', extra_attribute={'ldap_attr': 'first_name'})

    # test ExtraAttributeAction
    class MockPArser:  # pylint: disable=E0102
        def __init__(self, *args, **kwargs):
            self.users = []
            assert len(args) == 1
            assert isinstance(args[0], FileType)
            assert kwargs['options']['extra_attribute'] == {'ldap_attr': 'first_name'}
            assert kwargs['options']['result'] == 'result'

        def parse(self):
            pass

    monkeypatch.setattr(oidc_cmd, 'DjangoUserLDIFParser', MockPArser)
    call_command(
        'load-ldif', '--extra-attribute', 'ldap_attr', 'first_name', '--result', 'result', ldif.strpath
    )


def test_resetpassword(simple_user):
    call_command('resetpassword', 'user')
    old_pass = simple_user.password
    simple_user.refresh_from_db()
    assert old_pass != simple_user.password


def test_sync_metadata(db):
    test_file = py.path.local(__file__).dirpath('metadata.xml').strpath
    call_command('sync-metadata', test_file, source='abcd')


def test_check_and_repair_managers_of_roles(db, capsys):
    default_ou = get_default_ou()
    admin_op = get_operation(ADMIN_OP)

    OrganizationalUnit.objects.create(name='Orgunit1', slug='orgunit1')
    role1 = Role.objects.create(name='Role 1', slug='role-1', ou=default_ou)
    perm1 = Permission.objects.create(
        operation=admin_op,
        target_id=role1.id,
        ou=default_ou,
        target_ct=ContentType.objects.get_for_model(Role),
    )

    manager_role1 = Role.objects.create(name='Managers of Role 1', slug='_a2-managers-of-role-role1')
    manager_role1.permissions.add(perm1)
    manager_role1.save()

    call_command('check-and-repair', '--repair', '--noinput')

    captured = capsys.readouterr()
    assert '"Managers of Role 1": no admin scope' in captured.out
    assert 'Managers of Role 1" wrong ou, should be "Default organizational unit"' in captured.out
    assert 'invalid permission "Management / role / Role 1": not manage_members operation' in captured.out
    assert (
        'invalid permission "Management / role / Role 1": not admin_scope and not self manage permission'
        in captured.out
    )
    assert (
        'invalid admin role "Managers of Role 1" wrong ou, should be "Default organizational unit" is "None"'
        in captured.out
    )

    perm1.refresh_from_db()
    assert perm1.ou is None
    manager_role1 = role1.get_admin_role()
    assert manager_role1.ou == get_default_ou()
    assert manager_role1.permissions.count() == 3
    assert manager_role1.permissions.get(
        operation=get_operation(MANAGE_MEMBERS_OP), target_id=manager_role1.id
    )
    assert manager_role1.permissions.get(operation=get_operation(MANAGE_MEMBERS_OP), target_id=role1.id)
    assert manager_role1.permissions.get(
        operation=get_operation(VIEW_OP),
        target_ct=ContentType.objects.get_for_model(ContentType),
        target_id=ContentType.objects.get_for_model(User).pk,
    )

    manage_members_op = get_operation(MANAGE_MEMBERS_OP)
    perm1.op = manage_members_op
    perm1.save()
    call_command('check-and-repair', '--repair', '--noinput')
    perm1 = Permission.objects.get(operation=manage_members_op, target_id=role1.id)
    assert perm1.ou is None


def test_check_and_delete_unused_permissions(db, capsys, simple_user):
    role1 = Role.objects.create(name='Role1', slug='role1')
    op1 = Operation.objects.create(slug='operation-1')
    used_perm = Permission.objects.create(
        operation=op1, target_id=role1.id, target_ct=ContentType.objects.get_for_model(Role)
    )
    role1.admin_scope = used_perm
    role1.save()
    Permission.objects.create(
        operation=op1, target_id=simple_user.id, target_ct=ContentType.objects.get_for_model(get_user_model())
    )

    call_command('check-and-repair', '--fake', '--noinput')
    n_perm = len(Permission.objects.all())

    call_command('check-and-repair', '--repair', '--noinput')
    assert len(Permission.objects.all()) == n_perm - 1


def test_check_identifiers_uniqueness(db, capsys, settings):
    settings.A2_USERNAME_IS_UNIQUE = False
    ou = get_default_ou()
    ou.email_is_unique = True
    ou.save()

    User.objects.create(username='foo', email='foo@example.net', first_name='Toto', last_name='Foo', ou=ou)
    User.objects.create(username='foo', email='bar@example.net', first_name='Bar', last_name='Foo', ou=ou)
    User.objects.create(username='bar', email='bar@example.net', first_name='Tutu', last_name='Bar', ou=ou)

    settings.A2_EMAIL_IS_UNIQUE = True
    settings.A2_USERNAME_IS_UNIQUE = True

    call_command('check-and-repair', '--repair', '--noinput')

    captured = capsys.readouterr()
    assert 'found 2 user accounts with same username' in captured.out
    assert 'found 2 user accounts with same email' in captured.out


def test_clean_unused_account_max_mails_per_period(settings, db, mailoutbox, freezer):
    ou = get_default_ou()
    ou.clean_unused_accounts_alert = 1
    ou.clean_unused_accounts_deletion = 2
    ou.save()
    settings.A2_CLEAN_UNUSED_ACCOUNTS_MAX_MAIL_PER_PERIOD = 4

    for i in range(100):
        User.objects.create(ou=ou, email='user-%s@example.com' % i, last_login=now())

    call_command('clean-unused-accounts')
    assert len(mailoutbox) == 0

    freezer.move_to(datetime.timedelta(days=1))
    call_command('clean-unused-accounts')
    # 4 alerts
    assert len(mailoutbox) == 4

    freezer.move_to(datetime.timedelta(days=1))
    call_command('clean-unused-accounts')
    # 4 new alerts and 4 deletions notifications
    assert len(mailoutbox) == 4 + 8


def test_clean_user_exports(settings, app, superuser, freezer):
    users = [User(username='user%s' % i) for i in range(10)]
    User.objects.bulk_create(users)

    # export directory does not exist yet
    call_command('clean-user-exports')

    resp = login(app, superuser, '/manage/users/')
    resp = resp.click('CSV').follow()
    file_creation_time = now()
    assert resp.click('Download CSV')

    freezer.move_to(file_creation_time + datetime.timedelta(days=5))
    call_command('clean-user-exports')
    assert resp.click('Download CSV')

    freezer.move_to(file_creation_time + datetime.timedelta(days=8))
    call_command('clean-user-exports')
    with pytest.raises(webtest.app.AppError):
        resp.click('Download CSV')
