# 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 json

import django_tables2 as tables
from django.urls import reverse
from django.utils.encoding import force_bytes, force_text
from webtest import Upload

from authentic2.a2_rbac.models import OrganizationalUnit, Role
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.custom_user.models import User

from .utils import login, request_select2, text_content

TABLES_MAJOR_VERSION = int(tables.__version__.split('.', maxsplit=1)[0])


def test_manager_role_export(app, admin, ou1, role_ou1, ou2, role_ou2):
    import csv

    response = login(app, admin, 'a2-manager-roles')

    export_response = response.click('Export')
    export = export_response.json

    assert list(export.keys()) == ['roles']
    assert len(export['roles']) == 2
    assert {role['slug'] for role in export['roles']} == {'role_ou1', 'role_ou2'}

    export_response = response.click('CSV', href='/export/')
    reader = csv.reader(
        [force_text(line) for line in export_response.body.split(force_bytes('\r\n'))], delimiter=','
    )
    rows = [row for row in reader]

    assert rows[0] == ['name', 'slug', 'members', 'ou']
    assert len(rows) - 2 == 2  # csv header and last EOL
    assert {row[1] for row in rows[1:3]} == {'role_ou1', 'role_ou2'}
    assert {row[3] for row in rows[1:3]} == {'OU1', 'OU2'}

    response.form.set('search-text', 'role_ou1')
    search_response = response.form.submit()

    export_response = search_response.click('Export')
    export = export_response.json

    assert list(export.keys()) == ['roles']
    assert len(export['roles']) == 1
    assert export['roles'][0]['slug'] == 'role_ou1'

    export_response = search_response.click('CSV', href='/export/')
    reader = csv.reader(
        [force_text(line) for line in export_response.body.split(force_bytes('\r\n'))], delimiter=','
    )
    rows = [row for row in reader]

    assert rows[0] == ['name', 'slug', 'members', 'ou']
    assert len(rows) - 2 == 1  # csv header and last EOL
    assert rows[1][1] == 'role_ou1'


def test_manager_role_name_uniqueness_single_ou(app, admin):
    response = login(app, admin, 'a2-manager-roles')

    response = response.click('Add')
    response.form.set('name', 'Role1')
    response = response.form.submit('Save').follow()
    response = response.click('Roles')
    assert response.pyquery('td.name').text() == 'Role1'

    response = response.click('Add')
    response.form.set('name', 'Role1')
    response = response.form.submit('Save')
    assert response.pyquery('.error').text() == 'Name already used'


def test_manager_role_name_uniqueness_multiple_ou(app, admin, ou1):
    response = login(app, admin, 'a2-manager-roles')

    response = response.click('Add')
    response.form.set('ou', str(ou1.id))
    response.form.set('name', 'Role1')
    response = response.form.submit('Save').follow()
    response = response.click('Roles')
    assert response.pyquery('td.name').text() == 'Role1'

    response = response.click('Add')
    response.form.set('ou', str(ou1.id))
    response.form.set('name', 'Role1')
    response = response.form.submit('Save')
    assert response.pyquery('.error').text() == 'Name already used'


def test_role_members_via(app, admin):
    user1 = User.objects.create(username='user1')
    user2 = User.objects.create(username='user2')
    role1 = Role.objects.create(name='role1')
    role2 = Role.objects.create(name='role2')

    role1.add_child(role2)
    user1.roles.add(role1)
    user2.roles.add(role2)

    response = login(app, admin, '/manage/roles/%s/' % role1.id)
    response.forms['search-form']['search-all_members'] = True
    response = response.forms['search-form'].submit()
    rows = list(
        zip(
            [text_content(el) for el in response.pyquery('tr td.username')],
            [text_content(el) for el in response.pyquery('tr td.direct')],
            [text_content(el) for el in response.pyquery('tr td.via')],
        )
    )
    assert rows == [
        ('user1', '✔', ''),
        ('user2', '✘', 'role2'),
    ]


def test_manager_role_import(app, admin, ou1, role_ou1, ou2, role_ou2):
    response = login(app, admin, 'a2-manager-roles')

    export_response = response.click('Export')
    export = export_response.json

    assert len(export['roles']) == 2
    assert not 'ous' in export
    Role.objects.filter(ou__in=[ou1, ou2]).delete()

    # import in OUs specified in export file
    resp = app.get('/manage/roles/')
    resp = resp.click('Import')
    resp.form['site_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
    resp = resp.form.submit().follow()

    assert Role.objects.filter(name=role_ou1.name, ou=ou1).exists()
    assert Role.objects.filter(name=role_ou2.name, ou=ou2).exists()
    Role.objects.filter(ou__in=[ou1, ou2]).delete()

    # import in custom OU
    resp = app.get('/manage/roles/')
    resp = resp.click('Import')
    resp.form['site_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
    resp.form['ou'] = get_default_ou().pk
    resp = resp.form.submit().follow()

    assert Role.objects.filter(name=role_ou1.name, ou=get_default_ou()).exists()
    assert Role.objects.filter(name=role_ou2.name, ou=get_default_ou()).exists()

    response.form.set('search-text', 'role_ou1')
    response.form.submit()

    export_response = response.click('Export')
    new_export = export_response.json
    assert len(export['roles']) == 2
    assert new_export['roles'][0]['uuid'] == export['roles'][0]['uuid']
    assert new_export['roles'][1]['uuid'] == export['roles'][1]['uuid']

    # import in custom OU while roles exist in another OU
    resp = app.get('/manage/roles/')
    resp = resp.click('Import')
    resp.form['site_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
    resp.form['ou'] = ou1.pk
    resp = resp.form.submit().follow()

    assert Role.objects.filter(name=role_ou1.name, ou=get_default_ou()).exists()
    assert Role.objects.filter(name=role_ou2.name, ou=get_default_ou()).exists()
    assert Role.objects.filter(ou=ou1).count() == 4

    # in case ous are present in export file, they must not be imported
    export['ous'] = [
        {
            "uuid": "27255f404cb140df9a577da76b59f285",
            "slug": "should_not_exist",
            "name": "should_not_exist",
        }
    ]
    resp = app.get('/manage/roles/')  # unselect ou1
    resp = resp.click('Import')
    resp.form['site_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
    resp.form['ou'] = get_default_ou().pk
    resp = resp.form.submit().follow()

    assert not OrganizationalUnit.objects.filter(slug="should_not_exist").exists()

    # missing ou in export file
    export = {"roles": [{"slug": "agent", "name": "Agent"}]}
    resp = app.get('/manage/roles/')
    resp = resp.click('Import')
    resp.form['site_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
    resp = resp.form.submit()
    assert 'Missing Organizational Unit for role: Agent' in resp.text

    resp.form['ou'] = get_default_ou().pk
    resp.form['site_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
    resp = resp.form.submit().follow()
    assert Role.objects.get(slug='agent')


def test_manager_role_import_selected_ou(app, admin, ou1, ou2):
    response = login(app, admin, 'a2-manager-roles')
    response.form.set('search-ou', ou2.pk)
    response = response.form.submit()
    response = response.click('Import')
    assert response.pyquery.find('select#id_ou option[selected]')[0].text == 'OU2'


def test_manager_role_import_ou_permission(app, admin, ou1, role_ou1, ou2, role_ou2, admin_ou1):
    resp = login(app, admin, 'a2-manager-roles')
    resp = resp.click('Export')
    export = resp.json
    assert len(export['roles']) == 2
    role_ou1.delete()
    role_ou2.delete()
    app.session.flush()  # logout

    resp = login(app, admin_ou1, 'a2-manager-roles')
    resp = resp.click('Import')
    resp.form['ou'] = ''
    resp.form['site_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
    resp = resp.form.submit()

    # importing fails because user has no permission on ou2
    assert 'missing permissions on Organizational Unit' in resp.text


def test_manager_role_add_selected_ou(app, admin, ou1, ou2):
    response = login(app, admin, '/manage/roles/')
    response = response.click('Add role')
    assert response.pyquery.find('select#id_ou option[selected]')[0].text == 'Default organizational unit'

    response = app.get('/manage/roles/')
    response.form.set('search-ou', ou2.pk)
    response = response.form.submit()
    response = response.click('Add role')
    assert response.pyquery.find('select#id_ou option[selected]')[0].text == 'OU2'


def test_roles_displayed_fields(app, admin, ou1, ou2):
    login(app, admin)
    role1 = Role.objects.create(name='role1')
    role2 = Role.objects.create(name='role2')

    user1 = User.objects.create(username='user1')
    user2 = User.objects.create(username='user2')
    user1.roles.add(role1)
    user2.roles.add(role1)
    role2.add_child(role1)  # indirect members

    assert role1.can_manage_members
    role2.can_manage_members = False  # user syncronized from LDAP
    role2.save()

    # check OUTable
    response = app.get('/manage/roles/')
    rows = list(
        zip(
            [text_content(el) for el in response.pyquery('tr td.name')],
            [text_content(el) for el in response.pyquery('tr td.member_count')],
        )
    )
    assert rows == [
        ('role1', '2'),
        ('role2 (LDAP)', '0'),
    ]

    # check UserRolesTable
    response = app.get('/manage/users/%s/roles/?search-ou=all' % user2.pk)
    rows = list(
        zip(
            [text_content(el) for el in response.pyquery('tr td.name')],
            [text_content(el) for el in response.pyquery('tr td.via')],
        )
    )
    assert rows == [
        ('role1', ''),
        ('role2 (LDAP)', 'role1 '),
    ]

    # check OuUserRolesTable
    response = app.get('/manage/users/%s/roles/?search-ou=' % user2.pk)
    rows = list(
        zip(
            [text_content(el) for el in response.pyquery('tr td.name')],
            [text_content(el) for el in response.pyquery('tr td.via')],
            [el.attrib.get('checked') for el in response.pyquery('tr td.member input')],
            [el.attrib.get('disabled') for el in response.pyquery('tr td.member input')],
        )
    )
    assert rows == [
        ('role1', '', 'checked', None),
        ('role2 (LDAP)', 'role1 ', None, 'disabled'),
    ]


def test_manager_role_csv_import(app, admin, ou1, ou2):
    roles_count = Role.objects.count()
    resp = login(app, admin, '/manage/roles/')

    resp = resp.click('CSV import')
    csv_header = b'name,slug,ou\n'
    csv_content = 'Role Name,role_slug,%s' % ou1.slug
    resp.form['import_file'] = Upload('t.csv', csv_header + csv_content.encode(), 'text/csv')
    resp.form.submit(status=302)
    assert Role.objects.get(name='Role Name', slug='role_slug', ou=ou1)
    assert Role.objects.count() == roles_count + 1

    csv_content = 'Role 2,role2,\nRole 3,,%s' % ou2.slug
    resp.form['import_file'] = Upload('t.csv', csv_header + csv_content.encode(), 'text/csv')
    resp.form.submit(status=302)
    assert Role.objects.get(name='Role 2', slug='role2', ou=get_default_ou())
    assert Role.objects.get(name='Role 3', slug='role-3', ou=ou2)
    assert Role.objects.count() == roles_count + 3

    # slug can be updated using name, name can be updated using slug
    csv_content = 'Role two,role2,\nRole 3,role-three,%s' % ou2.slug
    resp.form['import_file'] = Upload('t.csv', csv_header + csv_content.encode(), 'text/csv')
    resp.form.submit(status=302)
    assert Role.objects.get(name='Role two', slug='role2', ou=get_default_ou())
    assert Role.objects.get(name='Role 3', slug='role-three', ou=ou2)
    assert Role.objects.count() == roles_count + 3

    # conflict in auto-generated slug is handled
    csv_header = b'name\n'
    csv_content = 'Role!2'
    resp.form['import_file'] = Upload('t.csv', csv_header + csv_content.encode(), 'text/csv')
    resp.form.submit(status=302)
    assert Role.objects.get(name='Role!2', slug='role2-1', ou=get_default_ou())
    assert Role.objects.count() == roles_count + 4

    # Identical roles are created only once
    csv_content = 'Role 4,role-4,\nRole 4,,\nRole 4,role-4,'
    resp.form['import_file'] = Upload('t.csv', csv_header + csv_content.encode(), 'text/csv')
    resp.form.submit(status=302)
    assert Role.objects.get(name='Role 4', slug='role-4', ou=get_default_ou())
    assert Role.objects.count() == roles_count + 5

    csv_content = 'xx\0xx,,'
    resp.form['import_file'] = Upload('t.csv', csv_header + csv_content.encode(), 'text/csv')
    resp = resp.form.submit()
    assert 'Invalid file format.' in resp.text

    wrong_header = b'a,b,c\n'
    resp.form['import_file'] = Upload('t.csv', wrong_header, 'text/csv')
    resp = resp.form.submit()
    assert 'Invalid file header' in resp.text

    csv_content = ',slug-but-no-name,\nRole,,unknown-ou'
    resp = app.get('/manage/roles/csv-import/')
    resp.form['import_file'] = Upload('t.csv', csv_header + csv_content.encode(), 'text/csv')
    resp = resp.form.submit()
    assert 'Name is required. (line 2)' in resp.text
    assert 'Organizational Unit unknown-ou does not exist. (line 3)' in resp.text

    resp = app.get('/manage/roles/csv-import/')
    resp = resp.click('Download sample')
    assert 'name,slug,ou' in resp.text


def test_role_members_display_role_parents(app, superuser, settings, simple_role):
    url = reverse('a2-manager-role-members', kwargs={'pk': simple_role.pk})

    resp = login(app, superuser, url)
    assert "This role doesn't contain permissions of any other role." in resp.text

    for i in range(10):
        role = Role.objects.create(name=f'Role {i}', ou=get_default_ou())
        simple_role.add_parent(role)

    resp = app.get(url)
    assert "This role doesn't contain permissions of any other role." not in resp.text

    for i, el in enumerate(resp.pyquery.find('a.role-inheritance-parent')):
        assert el.text == f'Role {i}'
    assert '(view all roles)' not in resp.text

    role = Role.objects.create(name='Role a', ou=get_default_ou())
    simple_role.add_parent(role)
    resp = app.get(url)
    assert 'Role a' not in resp.text
    assert '(view all roles)' in resp.text

    resp = resp.click('(view all roles)')
    assert 'Role a' in resp.text

    # display OU if there are more than one
    ou1 = OrganizationalUnit.objects.create(name='ou1')
    resp = app.get(url)
    for i, el in enumerate(resp.pyquery.find('a.role-inheritance-parent')):
        assert el.text == f'Default organizational unit - Role {i}'

    # sort by OU, then name
    Role.objects.filter(name__in=['Role 2', 'Role 3', 'Role 4']).update(ou=ou1)
    Role.objects.filter(name__in=['Role 5', 'Role 6']).update(ou=None)

    resp = app.get(url)
    assert [el.text for el in resp.pyquery.find('a.role-inheritance-parent')] == [
        'Role 5',
        'Role 6',
        'Default organizational unit - Role 0',
        'Default organizational unit - Role 1',
        'Default organizational unit - Role 7',
        'Default organizational unit - Role 8',
        'Default organizational unit - Role 9',
        'Default organizational unit - Role a',
        'ou1 - Role 2',
        'ou1 - Role 3',
    ]


def test_role_members_user_role_mixed_table(app, superuser, settings, simple_role, simple_user):
    simple_user.roles.add(simple_role)

    url = reverse('a2-manager-role-members', kwargs={'pk': simple_role.pk})
    resp = login(app, superuser, url)

    # no children, directly display members details
    assert resp.forms['search-form']['search-all_members'].value == 'on'
    assert 'disabled' in resp.forms['search-form']['search-all_members'].attrs
    assert 'Download list as CSV' in resp.text

    column_names = [text_content(el) for el in resp.pyquery('table#user-table th') if text_content(el)]

    # weird capitalisation discrepancy in django-tables2
    if TABLES_MAJOR_VERSION >= 2:
        assert column_names == [
            'User',
            'Username',
            'Email address',
            'First name',
            'Last name',
            'Organizational unit',
            'Direct member',
            'Inherited from',
        ]
    else:
        assert column_names == [
            'User',
            'Username',
            'Email Address',
            'First Name',
            'Last Name',
            'Organizational Unit',
            'Direct member',
            'Inherited from',
        ]

    rows = [text_content(el) for el in resp.pyquery('tr td.link')]
    assert rows == ['Jôhn Dôe']

    # add child
    role = Role.objects.create(name='Role a', ou=get_default_ou())
    user = User.objects.create(username='user1')
    user.roles.add(role)
    simple_role.add_child(role)

    resp = app.get(url)
    assert not resp.forms['search-form']['search-all_members'].value
    assert 'disabled' not in resp.forms['search-form']['search-all_members'].attrs
    assert 'Download list as CSV' not in resp.text

    column_names = [text_content(el) for el in resp.pyquery('table#user-table th') if text_content(el)]
    assert column_names == ['Members']

    rows = [text_content(el) for el in resp.pyquery('tr td.name')]
    assert rows == ['Members of role Role a', 'Jôhn Dôe']

    resp.forms['search-form']['search-all_members'] = True
    resp = resp.forms['search-form'].submit()

    assert resp.forms['search-form']['search-all_members'].value == 'on'
    assert 'disabled' not in resp.forms['search-form']['search-all_members'].attrs
    assert 'Download list as CSV' in resp.text

    rows = [text_content(el) for el in resp.pyquery('tr td.link')]
    assert rows == ['user1', 'Jôhn Dôe']

    resp = resp.click('Add a role as a member')
    assert 'Role a' in resp.text

    # add child role to child
    grandchild = Role.objects.create(name='grandchild')
    role.add_child(grandchild)

    resp = app.get(url)
    rows = [text_content(el) for el in resp.pyquery('tr td.name')]
    assert rows == ['Members of role Role a', 'Members of role grandchild', 'Jôhn Dôe']

    # remove icon is not shown for indirect child
    assert len(resp.pyquery('tr td a.js-remove-object')) == 2


def test_role_members_user_role_mixed_field_choices(
    app, superuser, settings, simple_role, simple_user, role_ou1
):
    url = reverse('a2-manager-role-members', kwargs={'pk': simple_role.pk})
    resp = login(app, superuser, url)

    select2_json = request_select2(app, resp)
    assert len(select2_json['results']) == 10
    assert select2_json['more'] is True

    select2_json = request_select2(app, resp, fetch_all=True)
    assert len(select2_json['results']) == 17
    choices = [x['text'] for x in select2_json['results']]
    assert choices == [
        'Default organizational unit - Managers of role "simple role"',
        'Default organizational unit - Roles - Default organizational unit',
        'Default organizational unit - Services - Default organizational unit',
        'Default organizational unit - Users - Default organizational unit',
        'OU1 - role_ou1',
        'OU1 - Roles - OU1',
        'OU1 - Services - OU1',
        'OU1 - Users - OU1',
        'Manager',
        'Manager of organizational units',
        'Manager of roles',
        'Manager of services',
        'Manager of users',
        'Managers of "Default organizational unit"',
        'Managers of "OU1"',
        'Jôhn Dôe - user@example.net - user',
        'super user - superuser@example.net - superuser',
    ]

    select2_json = request_select2(app, resp, term='user')
    choices = [x['text'] for x in select2_json['results']]
    assert choices == [
        'Default organizational unit - Users - Default organizational unit',
        'OU1 - Users - OU1',
        'Manager of users',
        'Jôhn Dôe - user@example.net - user',
        'super user - superuser@example.net - superuser',
    ]
    assert select2_json['more'] is False

    select2_json = request_select2(app, resp, term='Manager')
    assert len(select2_json['results']) == 8
    select2_json = request_select2(app, resp, term='Manager of')
    assert len(select2_json['results']) == 7
    select2_json = request_select2(app, resp, term='Manager of serv')
    assert len(select2_json['results']) == 1

    for i in range(25):
        Role.objects.create(name=f'test_role_{i}')
    select2_json = request_select2(app, resp, term='test_role_', fetch_all=True)
    assert len(select2_json['results']) == 25

    for i in range(25):
        User.objects.create(username=f'test_user_{i}')
    select2_json = request_select2(app, resp, term='test_user_', fetch_all=True)
    assert len(select2_json['results']) == 25

    for i in range(10):
        Role.objects.create(name=f'test_xxx_{i}')
    User.objects.create(username='test_xxx_10')
    select2_json = request_select2(app, resp, term='test_xxx_')
    assert len(select2_json['results']) == 11


def test_role_members_user_role_add_remove(app, superuser, settings, simple_role, simple_user, role_ou1):
    url = reverse('a2-manager-role-members', kwargs={'pk': simple_role.pk})
    resp = login(app, superuser, url)

    select2_json = request_select2(app, resp, term='Jôhn')
    assert len(select2_json['results']) == 1
    form = resp.forms['add-member']
    form['user_or_role'].force_value(select2_json['results'][0]['id'])
    resp = form.submit().follow()
    assert 'Jôhn Dôe' in resp.text

    select2_json = request_select2(app, resp, term='Jôhn')
    assert len(select2_json['results']) == 0

    data_pks = [row.attrib['data-pk'] for row in resp.pyquery('table tbody tr')]
    assert data_pks == ['user-%s' % simple_user.pk]
    data_pk_args = [row.attrib['data-pk-arg'] for row in resp.pyquery('table tbody tr td a.js-remove-object')]
    assert data_pk_args == ['user_or_role']

    select2_json = request_select2(app, resp, term='role_ou1')
    assert len(select2_json['results']) == 1
    form = resp.forms['add-member']
    form['user_or_role'].force_value(select2_json['results'][0]['id'])
    resp = form.submit().follow()
    assert 'role_ou1' in resp.text

    select2_json = request_select2(app, resp, term='role_ou1')
    assert len(select2_json['results']) == 0

    data_pks = [row.attrib['data-pk'] for row in resp.pyquery('table tbody tr')]
    assert data_pks == ['role-%s' % role_ou1.pk, 'user-%s' % simple_user.pk]
    data_pk_args = [row.attrib['data-pk-arg'] for row in resp.pyquery('table tbody tr td a.js-remove-object')]
    assert data_pk_args == ['user_or_role', 'user_or_role']

    # simulate click on Jôhn Dôe delete icon
    token = str(resp.context['csrf_token'])
    params = {'action': 'remove', 'user_or_role': 'user-%s' % simple_user.pk, 'csrfmiddlewaretoken': token}
    resp = app.post('/manage/roles/%s/' % simple_role.pk, params=params).follow()
    assert 'Jôhn Dôe' not in resp.text

    # simulate click on role_ou1 delete icon
    token = str(resp.context['csrf_token'])
    params = {'action': 'remove', 'user_or_role': 'role-%s' % role_ou1.pk, 'csrfmiddlewaretoken': token}
    resp = app.post('/manage/roles/%s/' % simple_role.pk, params=params).follow()
    assert 'role_ou1' not in resp.text

    # invalid choices are ignored
    for invalid_choice in ('', 'wrong-wrong', 'user-', 'user-xxx', 'role', 'user-99999'):
        form = resp.forms['add-member']
        form['user_or_role'].force_value(invalid_choice)
        resp = form.submit().maybe_follow()
