# 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/>.

from urllib.parse import urlparse

import pytest
from django.urls import reverse

from authentic2.a2_rbac.models import Role
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.models import APIClient, Attribute

from .utils import login


class TestAuthorization:
    @pytest.fixture
    def app(self, app, user):
        login(app, user)
        return app

    @pytest.fixture
    def api_client(self, db, ou1):
        return APIClient.objects.create(
            name='foo',
            description='foo-description',
            identifier='foo-description',
            password='foo-password',
            ou=ou1,
        )

    class Mixin:
        status_code = -1
        existing_client_status_code = -1

        def test_list(self, app):
            app.get(reverse('a2-manager-api-clients'), status=self.status_code)

        def test_add(self, app):
            app.get(reverse('a2-manager-api-client-add'), status=self.status_code)

        def test_detail(self, app, api_client):
            app.get(
                reverse('a2-manager-api-client-detail', kwargs={'pk': api_client.pk}),
                status=self.existing_client_status_code,
            )

        def test_edit(self, app, api_client):
            app.get(
                reverse('a2-manager-api-client-edit', kwargs={'pk': api_client.pk}),
                status=self.existing_client_status_code,
            )

        def test_delete(self, app, api_client):
            app.get(
                reverse('a2-manager-api-client-delete', kwargs={'pk': api_client.pk}),
                status=self.existing_client_status_code,
            )

    class TestAuthorization(Mixin):
        status_code = 403
        existing_client_status_code = 404

        @pytest.fixture
        def user(self, simple_user):
            return simple_user

    class TestAuthorizationLocalAdminWrongOu(Mixin):
        status_code = 200
        existing_client_status_code = 404

        @pytest.fixture
        def user(self, admin_ou2):
            return admin_ou2

    class TestAuthorizationLocalAdminRightOu(Mixin):
        status_code = 200
        existing_client_status_code = 200

        @pytest.fixture
        def user(self, admin_ou1):
            return admin_ou1

    class TestAuthorizationAdmin(Mixin):
        status_code = 200
        existing_client_status_code = 200

        @pytest.fixture
        def user(self, simple_user):
            simple_user.roles.add(Role.objects.get(ou__isnull=True, slug='_a2-manager-of-api-clients'))
            return simple_user


def test_list_empty(superuser, app):
    resp = login(app, superuser, 'a2-manager-api-clients')
    assert 'There are no API client defined.' in resp.text


def test_list_add_button(superuser, app):
    resp = login(app, superuser, 'a2-manager-api-clients')
    anchor = resp.pyquery('span.actions a[href="%s"]' % reverse('a2-manager-api-client-add'))
    assert anchor.text() == 'Add new API client'


def test_list_show_objects(superuser, app):
    api_client = APIClient.objects.create(
        name='foo', description='foo-description', identifier='foo-description', password='foo-password'
    )
    url = '/manage/api-clients/%s/' % api_client.pk
    resp = login(app, superuser, 'a2-manager-api-clients')
    anchor = resp.pyquery('div.content ul.objects-list a[href="%s"]' % url)
    assert anchor.text() == 'foo (foo-description) - Default organizational unit'


def test_list_show_objects_local_admin(admin_ou1, app, ou1, ou2):
    api_client_ou1 = APIClient.objects.create(
        name='foo',
        description='foo-description',
        identifier='foo-description',
        password='foo-password',
        ou=ou1,
    )
    api_client_ou2 = APIClient.objects.create(
        name='bar',
        description='bar-description',
        identifier='bar-description',
        password='bar-password',
        ou=ou2,
    )
    url = '/manage/api-clients/%s/' % api_client_ou1.pk
    resp = login(app, admin_ou1, 'a2-manager-api-clients')
    assert len(resp.pyquery('div.content ul.objects-list li')) == 1
    anchor = resp.pyquery('div.content ul.objects-list a[href="%s"]' % url)
    assert anchor.text() == 'foo (foo-description) - OU1'

    role = Role.objects.get(slug='_a2-manager-of-api-clients-%s' % ou2.slug)
    admin_ou1.roles.add(role)
    admin_ou1.save()
    resp = app.get(reverse('a2-manager-api-clients'))
    assert len(resp.pyquery('div.content ul.objects-list li')) == 2
    anchor = resp.pyquery('div.content ul.objects-list a[href="%s"]' % url)
    assert anchor.text() == 'foo (foo-description) - OU1'
    url = '/manage/api-clients/%s/' % api_client_ou2.pk
    anchor = resp.pyquery('div.content ul.objects-list a[href="%s"]' % url)
    assert anchor.text() == 'bar (bar-description) - OU2'


def test_add(superuser, app):
    preferred_color = Attribute.objects.create(
        name='preferred_color',
        label='Preferred color',
        kind='string',
        disabled=False,
        multiple=False,
    )
    phone2 = Attribute.objects.create(
        name='phone2',
        label='Second phone number',
        kind='phone_number',
        disabled=False,
        multiple=False,
    )
    assert APIClient.objects.count() == 0
    role_1 = Role.objects.create(name='role-1', ou=get_default_ou())
    role_2 = Role.objects.create(name='role-2', ou=get_default_ou())
    resp = login(app, superuser, 'a2-manager-api-client-add')
    form = resp.form
    # password is prefilled
    assert form.get('password').value
    assert ('', False, '---------') in form['ou'].options
    form.set('name', 'api-client-name')
    form.set('description', 'api-client-description')
    form.set('identifier', 'api-client-identifier')
    form.set('password', 'api-client-password')
    form['apiclient_roles'].force_value([role_1.id, role_2.id])
    form.set('allowed_user_attributes', [preferred_color.id, phone2.id])
    response = form.submit().follow()
    assert APIClient.objects.count() == 1
    api_client = APIClient.objects.get(name='api-client-name')
    assert set(api_client.apiclient_roles.all()) == {role_1, role_2}
    assert set(api_client.allowed_user_attributes.all()) == {preferred_color, phone2}
    assert urlparse(response.request.url).path == api_client.get_absolute_url()


def test_add_local_admin(admin_ou1, app, ou1, ou2):
    assert APIClient.objects.count() == 0
    resp = login(app, admin_ou1, 'a2-manager-api-client-add')
    form = resp.form
    assert len(form['ou'].options) == 1
    assert ('', False, '---------') not in form['ou'].options
    assert form['ou'].options[0][2] == 'OU1'

    role = Role.objects.get(slug='_a2-manager-of-api-clients-%s' % ou2.slug)
    admin_ou1.roles.add(role)
    resp = app.get(reverse('a2-manager-api-client-add'))
    assert len(resp.form['ou'].options) == 2
    assert ('', False, '---------') not in form['ou'].options


def test_add_description_non_mandatory(superuser, app):
    assert APIClient.objects.count() == 0
    role_1 = Role.objects.create(name='role-1', ou=get_default_ou())
    role_2 = Role.objects.create(name='role-2', ou=get_default_ou())
    resp = login(app, superuser, 'a2-manager-api-client-add')
    form = resp.form
    form.set('name', 'api-client-name')
    form.set('identifier', 'api-client-identifier')
    form.set('password', 'api-client-password')
    form['apiclient_roles'].force_value([role_1.id, role_2.id])
    response = form.submit().follow()
    assert APIClient.objects.count() == 1
    api_client = APIClient.objects.get(name='api-client-name')
    assert set(api_client.apiclient_roles.all()) == {role_1, role_2}
    assert urlparse(response.request.url).path == api_client.get_absolute_url()


def test_detail(superuser, app, phone_activated_authn):
    role_1 = Role.objects.create(name='role-1')
    role_2 = Role.objects.create(name='role-2')
    api_client = APIClient.objects.create(
        name='foo',
        description='foo-description',
        identifier='foo-identifier',
        password='foo-password',
        restrict_to_anonymised_data=True,
    )
    api_client.apiclient_roles.add(role_1, role_2)
    api_client.allowed_user_attributes.add(phone_activated_authn.phone_identifier_field)
    resp = login(app, superuser, api_client.get_absolute_url())
    assert 'foo-description' in resp.text
    assert 'Identifier: foo-identifier' in resp.text
    assert 'Password: foo-password' in resp.text
    assert 'foo-description' in resp.text
    assert 'Restricted to anonymised data' in resp.text
    assert 'role-1' in resp.text
    assert 'role-2' in resp.text
    assert 'Allowed user attributes' in resp.text
    assert 'phone' in resp.text

    edit_button = resp.pyquery(
        'span.actions a[href="%s"]' % reverse('a2-manager-api-client-edit', kwargs={'pk': api_client.pk})
    )
    assert edit_button
    assert edit_button.text() == 'Edit'
    delete_button = resp.pyquery(
        'span.actions a[href="%s"]' % reverse('a2-manager-api-client-delete', kwargs={'pk': api_client.pk})
    )
    assert delete_button
    assert delete_button.text() == 'Delete'


def test_manager_apiclient_roles_list(app, superuser):
    login(app, superuser, '/')

    role = Role.objects.create(name='parent', slug='parent', ou=get_default_ou())
    api_client = APIClient.objects.create(name='foo')
    api_client.apiclient_roles.set([role.pk])

    resp = app.get(reverse('a2-manager-api-client-detail', kwargs={'pk': api_client.pk}))

    assert 'href="%s"' % (reverse('a2-manager-role-members', kwargs={'pk': role.pk})) in resp.text


def test_edit(superuser, app, ou1, ou2):
    preferred_color = Attribute.objects.create(
        name='preferred_color',
        label='Preferred color',
        kind='string',
        disabled=False,
        multiple=False,
    )
    phone2 = Attribute.objects.create(
        name='phone2',
        label='Second phone number',
        kind='phone_number',
        disabled=False,
        multiple=False,
    )
    role_1 = Role.objects.create(name='role-1', ou=ou1)
    role_2 = Role.objects.create(name='role-2', ou=ou2)
    role_3 = Role.objects.create(name='role-3', ou=ou1)
    api_client = APIClient.objects.create(
        name='foo',
        description='foo-description',
        identifier='foo-identifier',
        password='foo-password',
        ou=ou1,
    )
    api_client.allowed_user_attributes.add(preferred_color, phone2)
    api_client.save()
    assert APIClient.objects.count() == 1
    resp = login(app, superuser, 'a2-manager-api-client-edit', kwargs={'pk': api_client.pk})
    form = resp.form
    assert form.get('password').value == 'foo-password'
    assert set(form.get('allowed_user_attributes').value) == {str(preferred_color.id), str(phone2.id)}
    assert ('', False, '---------') in form['ou'].options
    resp.form.set('password', 'easy')
    with pytest.raises(KeyError):
        # forcing values not presented by the Select2ModelMultipleChoiceField,
        # should not happen in UI
        form['apiclient_roles'].force_value([role_1.id, role_2.id])
        form.submit()
    form['apiclient_roles'].force_value([role_1.id, role_3.id])
    form['allowed_user_attributes'].force_value([phone2.id])
    response = form.submit().follow()
    assert urlparse(response.request.url).path == api_client.get_absolute_url()
    assert APIClient.objects.count() == 1
    api_client = APIClient.objects.get(password='easy')
    assert api_client.identifier == 'foo-identifier'
    assert set(api_client.allowed_user_attributes.all()) == {phone2}

    resp = app.get(reverse('a2-manager-api-client-edit', kwargs={'pk': api_client.pk}))
    form = resp.form
    form.set('ou', ou2.id)
    response = form.submit()
    assert (
        response.pyquery('.error').text()
        == 'The following roles do not belong to organizational unit OU2: role-1, role-3.'
    )
    response.form.set('ou', ou2.id)
    response.form['apiclient_roles'].force_value([])
    response.form['allowed_user_attributes'].force_value([])
    response.form.submit().follow()
    api_client = APIClient.objects.get()
    assert set(api_client.apiclient_roles.all()) == set()
    assert set(api_client.allowed_user_attributes.all()) == set()
    assert api_client.ou == ou2

    resp = app.get(reverse('a2-manager-api-client-edit', kwargs={'pk': api_client.pk}))
    form = resp.form
    form['apiclient_roles'].force_value([role_2.id])
    form['allowed_user_attributes'].force_value([preferred_color.id])
    response = form.submit().follow()
    api_client = APIClient.objects.get()
    assert api_client.ou == ou2
    assert set(api_client.apiclient_roles.all()) == {role_2}
    assert set(api_client.allowed_user_attributes.all()) == {preferred_color}


def test_edit_local_admin(admin_ou1, app, ou1, ou2):
    role_1 = Role.objects.create(name='role-1', ou=ou1)
    role_2 = Role.objects.create(name='role-2', ou=ou2)
    role_3 = Role.objects.create(name='role-3', ou=ou1)
    api_client_ou1 = APIClient.objects.create(
        name='foo',
        description='foo-description',
        identifier='foo-description',
        password='foo-password',
        ou=ou1,
    )
    api_client_ou2 = APIClient.objects.create(
        name='bar',
        description='bar-description',
        identifier='bar-description',
        password='bar-password',
        ou=ou2,
    )
    resp = login(app, admin_ou1, 'a2-manager-api-client-edit', kwargs={'pk': api_client_ou1.pk})
    form = resp.form
    assert form.get('password').value == 'foo-password'
    resp.form.set('password', 'easy')
    assert ('', False, '---------') not in form['ou'].options
    with pytest.raises(KeyError):
        # forcing values not presented by the Select2ModelMultipleChoiceField,
        # should not happen in UI
        form['apiclient_roles'].force_value([role_1.id, role_2.id])
        form.submit()
    form['apiclient_roles'].force_value([role_1.id, role_3.id])
    response = form.submit().follow()
    assert urlparse(response.request.url).path == api_client_ou1.get_absolute_url()
    api_client = APIClient.objects.get(password='easy')
    assert api_client.identifier == 'foo-description'

    role = Role.objects.get(slug='_a2-manager-of-api-clients-%s' % ou2.slug)
    admin_ou1.roles.add(role)
    resp = app.get(reverse('a2-manager-api-client-edit', kwargs={'pk': api_client_ou2.pk}))
    assert resp.form.get('password').value == 'bar-password'
    assert ('', False, '---------') not in form['ou'].options
    resp.form.set('ou', ou1.id)
    resp.form.submit().follow()
    assert APIClient.objects.filter(ou=ou1).count() == 2


def test_delete(superuser, app):
    api_client = APIClient.objects.create(
        name='foo', description='foo-description', identifier='foo-identifier', password='foo-password'
    )
    assert APIClient.objects.count() == 1
    resp = login(app, superuser, 'a2-manager-api-client-delete', kwargs={'pk': api_client.pk})
    response = resp.form.submit().follow()
    assert urlparse(response.request.url).path == reverse('a2-manager-api-clients')
    assert APIClient.objects.count() == 0
