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

import datetime
from urllib.parse import urlparse

import pytest
from django.urls import reverse
from django.utils.html import escape

from authentic2.custom_user.models import DeletedUser, User

from .utils import assert_event, get_link_from_mail, login, logout

pytestmark = pytest.mark.django_db


def test_profile(app, simple_user):
    page = login(app, simple_user, path=reverse('account_management'))
    assert simple_user.first_name in page
    assert simple_user.last_name in page


def test_password_change(app, simple_user):
    simple_user.set_password('hop')
    simple_user.save()
    resp = login(app, simple_user, password='hop', path=reverse('password_change'))
    old_session_key = app.session.session_key

    resp.form['old_password'] = 'hop'
    resp.form['new_password1'] = 'hopAbcde1'
    resp.form['new_password2'] = 'hopAbcde1'
    resp = resp.form.submit()

    new_session_key = app.session.session_key

    assert old_session_key != new_session_key, 'session\'s key has not been cycled'

    assert resp.location == '/accounts/password/change/done/'
    assert_event('user.password.change', user=simple_user, session=app.session)


def test_account_delete(app, simple_user, mailoutbox):
    assert simple_user.is_active
    assert not len(mailoutbox)
    page = login(app, simple_user, path=reverse('delete_account'))
    assert simple_user.email in page.text
    page.form.submit(name='submit').follow()
    assert len(mailoutbox) == 1
    link = get_link_from_mail(mailoutbox[0])
    assert 'Validate account deletion request on testserver' == mailoutbox[0].subject
    assert [simple_user.email] == mailoutbox[0].to
    page = app.get(link)
    # FIXME: webtest does not set the Referer header, so the logout page will always ask for
    # confirmation under tests
    response = page.form.submit(name='delete')
    assert '_auth_user_id' not in app.session
    assert User.objects.filter(id=simple_user.id).count() == 0
    assert DeletedUser.objects.filter(old_user_id=simple_user.id).count() == 1
    assert len(mailoutbox) == 2
    assert 'Account deletion on testserver' == mailoutbox[1].subject
    assert mailoutbox[0].to == [simple_user.email]
    assert "Deletion performed" in str(response)  # Set-Cookie: messages..
    assert urlparse(response.location).path == '/'


def test_account_delete_when_logged_out(app, simple_user, mailoutbox):
    assert simple_user.is_active
    assert not len(mailoutbox)
    page = login(app, simple_user, path=reverse('delete_account'))
    page.form.submit(name='submit').follow()
    assert len(mailoutbox) == 1
    link = get_link_from_mail(mailoutbox[0])
    logout(app)
    page = app.get(link)
    assert (
        'You are about to delete the account of <strong>%s</strong>.' % escape(simple_user.get_full_name())
        in page.text
    )
    response = page.form.submit(name='delete')
    assert User.objects.filter(id=simple_user.id).count() == 0
    assert DeletedUser.objects.filter(old_user_id=simple_user.id).count() == 1
    assert len(mailoutbox) == 2
    assert 'Account deletion on testserver' == mailoutbox[1].subject
    assert mailoutbox[0].to == [simple_user.email]
    assert "Deletion performed" in str(response)  # Set-Cookie: messages..
    assert urlparse(response.location).path == '/'


def test_account_delete_by_other_user(app, simple_user, user_ou1, mailoutbox):
    assert simple_user.is_active
    assert user_ou1.is_active
    assert not len(mailoutbox)
    page = login(app, simple_user, path=reverse('delete_account'))
    page.form.submit(name='submit').follow()
    assert len(mailoutbox) == 1
    link = get_link_from_mail(mailoutbox[0])
    logout(app)
    login(app, user_ou1, path=reverse('account_management'))
    page = app.get(link)
    assert (
        'You are about to delete the account of <strong>%s</strong>.' % escape(simple_user.get_full_name())
        in page.text
    )
    response = page.form.submit(name='delete')
    assert app.session['_auth_user_id'] == str(user_ou1.id)
    assert User.objects.filter(id=simple_user.id).count() == 0
    assert DeletedUser.objects.filter(old_user_id=simple_user.id).count() == 1
    assert len(mailoutbox) == 2
    assert 'Account deletion on testserver' == mailoutbox[1].subject
    assert mailoutbox[0].to == [simple_user.email]
    assert "Deletion performed" in str(response)  # Set-Cookie: messages..
    assert urlparse(response.location).path == '/'


def test_account_delete_fake_token(app, simple_user, mailoutbox):
    response = (
        app.get(reverse('validate_deletion', kwargs={'deletion_token': 'thisismostlikelynotavalidtoken'}))
        .follow()
        .follow()
    )
    assert "The account deletion request is invalid, try again" in response.text


def test_account_delete_expired_token(app, simple_user, mailoutbox, freezer):
    freezer.move_to('2019-08-01')
    page = login(app, simple_user, path=reverse('delete_account'))
    page.form.submit(name='submit').follow()
    freezer.move_to('2019-08-04')  # Too late...
    link = get_link_from_mail(mailoutbox[0])
    response = app.get(link).follow()
    assert "The account deletion request is too old, try again" in response.text


def test_account_delete_valid_token_unexistent_user(app, simple_user, mailoutbox):
    page = login(app, simple_user, path=reverse('delete_account'))
    page.form.submit(name='submit').follow()
    link = get_link_from_mail(mailoutbox[0])
    simple_user.delete()
    response = app.get(link).follow().follow()
    assert 'This account has previously been deleted.' in response.text


def test_account_delete_valid_token_inactive_user(app, simple_user, mailoutbox):
    page = login(app, simple_user, path=reverse('delete_account'))
    page.form.submit(name='submit').follow()
    link = get_link_from_mail(mailoutbox[0])
    simple_user.is_active = False
    simple_user.save()
    response = app.get(link).maybe_follow()
    assert 'This account is inactive, it cannot be deleted.' in response.text


def test_login_invalid_next(app):
    app.get(reverse('auth_login') + '?next=plop')


def test_custom_account(settings, app, simple_user):
    response = login(app, simple_user, path=reverse('account_management'))
    assert response.status_code == 200
    settings.A2_ACCOUNTS_URL = 'http://combo/account/'
    response = app.get(reverse('account_management'))
    assert response.status_code == 302
    assert response['Location'] == settings.A2_ACCOUNTS_URL


@pytest.mark.parametrize('view_name', ['registration_register', 'password_reset'])
def test_views_email_ratelimit(app, db, simple_user, settings, mailoutbox, freezer, view_name):
    freezer.move_to('2020-01-01')
    settings.A2_EMAILS_IP_RATELIMIT = '10/h'
    settings.A2_EMAILS_ADDRESS_RATELIMIT = '3/d'
    users = [User.objects.create(email='test%s@test.com' % i) for i in range(8)]

    # reach email limit
    for _ in range(3):
        response = app.get(reverse(view_name))
        response.form.set('email', simple_user.email)
        response = response.form.submit()
    assert len(mailoutbox) == 3

    response = app.get(reverse(view_name))
    response.form.set('email', simple_user.email)
    response = response.form.submit()
    assert len(mailoutbox) == 3
    assert 'try again later' in response.text
    if view_name == 'password_reset':
        assert_event('user.password.reset.failure', email=simple_user.email)

    # reach ip limit
    for i in range(7):
        response = app.get(reverse(view_name))
        response.form.set('email', users[i].email)
        response = response.form.submit()
    assert len(mailoutbox) == 10

    response = app.get(reverse(view_name))
    response.form.set('email', users[i + 1].email)
    response = response.form.submit()
    assert len(mailoutbox) == 10
    assert 'try again later' in response.text

    # ip ratelimits are lifted after an hour
    freezer.tick(datetime.timedelta(hours=1))
    response = app.get(reverse(view_name))
    response.form.set('email', users[0].email)
    response = response.form.submit()
    assert len(mailoutbox) == 11

    # email ratelimits are lifted after a day
    response = app.get(reverse(view_name))
    response.form.set('email', simple_user.email)
    response = response.form.submit()
    assert len(mailoutbox) == 11
    assert 'try again later' in response.text

    freezer.tick(datetime.timedelta(days=1))
    response = app.get(reverse(view_name))
    response.form.set('email', simple_user.email)
    response = response.form.submit()
    assert len(mailoutbox) == 12


@pytest.mark.parametrize('view_name', ['registration_register', 'password_reset'])
def test_views_email_token_resend(app, simple_user, settings, mailoutbox, view_name):
    settings.A2_TOKEN_EXISTS_WARNING = True

    response = app.get(reverse(view_name))
    response.form.set('email', simple_user.email)
    response = response.form.submit()
    assert len(mailoutbox) == 1

    # warn user token has already been sent
    response = app.get(reverse(view_name))
    response.form.set('email', simple_user.email)
    response = response.form.submit()
    assert 'email has already been sent' in response.text
    assert len(mailoutbox) == 1

    # validating again anyway works
    response = response.form.submit()
    assert len(mailoutbox) == 2


def test_views_login_display_a_cancel_button(app, settings):
    response = app.get(reverse('auth_login'), params={'next': '/foo/', 'nonce': 'xxx'})
    assert not response.html.find('button', {'class': 'cancel-button'})

    settings.A2_LOGIN_DISPLAY_A_CANCEL_BUTTON = True
    response = app.get(reverse('auth_login'), params={'next': '/foo/', 'nonce': 'xxx'})
    assert response.html.find('button', {'class': 'cancel-button'})
