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

import pytest
from django.contrib.auth import get_user_model

from authentic2 import models

from .utils import assert_event, check_log, login

User = get_user_model()


def test_success(db, app, simple_user):
    login(app, simple_user)
    assert_event('user.login', user=simple_user, session=app.session, how='password-on-https')
    session = app.session
    app.get('/logout/').form.submit()
    assert_event('user.logout', user=simple_user, session=session)


def test_failure(db, app, simple_user):
    login(app, simple_user, password='wrong', fail=True)
    assert_event('user.login.failure', user=simple_user, username=simple_user.username)

    login(app, 'noone', password='wrong', fail=True)
    assert_event('user.login.failure', username='noone')


def test_login_inactive_user(db, app):
    user1 = User.objects.create(username='john.doe')
    user1.set_password('john.doe')
    user1.save()
    user2 = User.objects.create(username='john.doe')
    user2.set_password('john.doe')
    user2.save()

    login(app, user1)
    assert int(app.session['_auth_user_id']) in [user1.id, user2.id]
    app.get('/logout/').form.submit()
    assert '_auth_user_id' not in app.session
    user1.is_active = False
    user1.save()
    login(app, user2)
    assert int(app.session['_auth_user_id']) == user2.id
    app.get('/logout/').form.submit()
    assert '_auth_user_id' not in app.session
    user2.is_active = False
    user2.save()
    with pytest.raises(AssertionError):
        login(app, user1)
    assert '_auth_user_id' not in app.session


def test_show_condition(db, app, settings, caplog):
    response = app.get('/login/')
    assert 'name="login-password-submit"' in response

    settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'False'}}
    response = app.get('/login/')
    # login form must not be displayed
    assert 'name="login-password-submit"' not in response
    assert len(caplog.records) == 0
    # set a condition with error

    settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': '\'admin\' in unknown'}}
    response = app.get('/login/')
    assert 'name="login-password-submit"' in response
    assert len(caplog.records) == 0


def test_show_condition_service(db, app, settings):
    settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'service_slug == \'portal\''}}
    response = app.get('/login/', params={'service': 'portal'})
    assert 'name="login-password-submit"' in response

    # Create a service
    models.Service.objects.create(name='Service', slug='portal')
    response = app.get('/login/', params={'service': 'portal'})
    assert 'name="login-password-submit"' in response

    models.Service.objects.create(name='Service', slug='service')
    response = app.get('/login/', params={'service': 'service'})
    assert 'name="login-password-submit"' not in response


def test_show_condition_with_headers(app, settings):
    settings.A2_AUTH_OIDC_ENABLE = False  # prevent db access by OIDC frontend
    settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': '\'X-Entrouvert\' in headers'}}
    response = app.get('/login/')
    assert 'name="login-password-submit"' not in response
    response = app.get('/login/', headers={'x-entrouvert': '1'})
    assert 'name="login-password-submit"' in response


def test_registration_url_on_login_page(db, app):
    response = app.get('/login/?next=/whatever')
    assert 'register/?next=/whatever"' in response


def test_redirect_login_to_homepage(db, app, settings, simple_user, superuser):
    settings.A2_LOGIN_REDIRECT_AUTHENTICATED_USERS_TO_HOMEPAGE = True
    login(app, simple_user)
    response = app.get('/login/')
    assert response.status_code == 302


def test_exponential_backoff(db, app, settings):
    settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION = 0
    response = app.get('/login/')
    response.form.set('username', '')
    response.form.set('password', 'zozo')
    response = response.form.submit('login-password-submit')
    assert response.status_code == 200

    for i in range(10):
        response.form.set('username', 'zozo')
        response.form.set('password', 'zozo')
        response = response.form.submit('login-password-submit')
        assert 'too many login' not in response.text

    settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION = 1.0
    settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION = 10.0

    for i in range(10):
        response.form.set('username', 'zozo')
        response.form.set('password', 'zozo')
        response = response.form.submit('login-password-submit')
        if 1.8 ** i > 10:
            break
        assert 'too many login' not in response.text, '%s' % i
    assert 'too many login' in response.text, '%s' % i


def test_encoded_utf8_in_next_url(app, db):
    url = '/manage/roles/?search-ou=all&search-text=r%C3%A9dacteur&search-internals=on'
    response = app.get(url)
    response = response.follow()
    needle = 'next=%s' % quote(url)
    assert needle in response.text


def test_session_expire(app, simple_user, freezer):
    freezer.move_to('2018-01-01')
    # Verify session work as usual
    login(app, simple_user)
    response = app.get('/')
    assert simple_user.first_name in response
    freezer.move_to('2018-01-15')
    response = app.get('/')
    assert simple_user.first_name not in response


def test_session_remember_me_ok(app, settings, simple_user, freezer):
    settings.A2_USER_REMEMBER_ME = 3600 * 24 * 30
    freezer.move_to('2018-01-01')
    # Verify session are longer
    login(app, simple_user, remember_me=True)

    response = app.get('/')
    assert simple_user.first_name in response

    # less than 30 days, session is still alive
    freezer.move_to('2018-01-30')
    response = app.get('/')
    assert simple_user.first_name in response


def test_session_remember_me_nok(app, settings, simple_user, freezer):
    settings.A2_USER_REMEMBER_ME = 3600 * 24 * 30
    freezer.move_to('2018-01-01')
    # Verify session are longer
    login(app, simple_user, remember_me=True)

    response = app.get('/')
    assert simple_user.first_name in response

    # more than 30 days, session is dead
    freezer.move_to('2018-01-31')
    response = app.get('/')
    assert simple_user.first_name not in response


def test_ou_selector(app, settings, simple_user, ou1, ou2, user_ou1, role_ou1):
    settings.A2_LOGIN_FORM_OU_SELECTOR = True
    response = app.get('/login/')
    # Check selector is here and there are no errors
    assert not response.pyquery('.errorlist')
    assert response.pyquery.find('select#id_ou')
    assert len(response.pyquery.find('select#id_ou optgroup')) == 0
    assert {elt.text for elt in response.pyquery.find('select#id_ou option')} == {
        'Default organizational unit',
        'OU1',
        'OU2',
        '---------',
    }
    # Check selector is required
    response.form.set('username', simple_user.username)
    response.form.set('password', simple_user.username)
    response = response.form.submit(name='login-password-submit')
    assert response.pyquery('.widget-with-error')
    # Check login to the wrong ou do not work
    response.form.set('password', simple_user.username)
    response.form.set('ou', str(ou1.pk))
    response = response.form.submit(name='login-password-submit')
    assert response.pyquery('.errornotice')
    assert '_auth_user_id' not in app.session
    # Check login to the proper ou works
    response.form.set('password', simple_user.username)
    response.form.set('ou', str(simple_user.ou.pk))
    response = response.form.submit(name='login-password-submit').follow()
    assert '_auth_user_id' in app.session
    response = response.click('Logout').maybe_follow()
    assert '_auth_user_id' not in app.session
    assert app.cookies['preferred-ous'] == str(simple_user.ou.pk)

    # Check last ou is preselected and shown first
    response = app.get('/login/')
    assert response.pyquery.find('select#id_ou')
    assert len(response.pyquery.find('select#id_ou optgroup')) == 2
    assert {elt.text for elt in response.pyquery.find('select#id_ou option')} == {
        'Default organizational unit',
        'OU1',
        'OU2',
        '---------',
    }
    assert response.pyquery.find('select#id_ou option[selected]')[0].text == 'Default organizational unit'

    # Create a service
    service = models.Service.objects.create(name='Service', slug='service', ou=ou1)
    response = app.get('/login/')
    assert response.pyquery.find('select#id_ou option[selected]')[0].text == 'Default organizational unit'

    # service is specified but not access-control is defined, default for user is selected
    response = app.get('/login/?service=service')
    assert response.pyquery.find('select#id_ou option[selected]')[0].text == 'Default organizational unit'

    # service is specified, access control is defined but role is empty, default for user is selected
    service.authorized_roles.through.objects.create(service=service, role=role_ou1)
    response = app.get('/login/?service=service')
    assert response.pyquery.find('select#id_ou option[selected]')[0].text == 'Default organizational unit'

    # user is added to role_ou1, default for user is still selected
    user_ou1.roles.add(role_ou1)
    response = app.get('/login/?service=service')
    assert response.pyquery.find('select#id_ou option[selected]')[0].text == 'Default organizational unit'

    # Clear cookies, OU1 is selected
    app.cookiejar.clear()
    response = app.get('/login/?service=service')
    assert response.pyquery.find('select#id_ou option[selected]')[0].text == 'OU1'

    # if we change the user's ou, then default selected OU changes
    user_ou1.ou = ou2
    user_ou1.save()
    response = app.get('/login/?service=service')
    assert response.pyquery.find('select#id_ou option[selected]')[0].text == 'OU2'


def test_login_test_cookie(app, simple_user):
    resp = app.get('/login/')
    # simulate browser blocking cooking by clearing the cookiejar
    app.cookiejar.clear()
    resp.form.set('username', simple_user.username)
    resp.form.set('password', simple_user.username)
    resp = resp.form.submit(name='login-password-submit')
    # CSRF and test cookie checks failed
    assert 'Cookies are disabled' in resp


def test_login_error_messages(app, settings, simple_user):
    settings.A2_USER_CAN_RESET_PASSWORD = True
    settings.REGISTRATION_OPEN = True
    resp = app.get('/login/')
    resp.form.set('username', 'x')
    resp.form.set('password', 'y')
    resp = resp.form.submit(name='login-password-submit')
    assert 'Incorrect Username or password.' in resp
    assert 'use the forgotten password link below' in resp
    assert 'or create an account.' in resp

    settings.A2_USER_CAN_RESET_PASSWORD = False
    settings.REGISTRATION_OPEN = False
    resp.form.set('username', 'x')
    resp.form.set('password', 'y')
    resp = resp.form.submit(name='login-password-submit')
    assert 'Incorrect Username or password.' in resp
    assert 'use the forgotten password link below' not in resp
    assert 'or create an account.' not in resp

    settings.A2_USER_CAN_RESET_PASSWORD = True
    settings.REGISTRATION_OPEN = False
    resp.form.set('username', 'x')
    resp.form.set('password', 'y')
    resp = resp.form.submit(name='login-password-submit')
    assert 'Incorrect Username or password.' in resp
    assert 'use the forgotten password link below' in resp
    assert 'or create an account.' not in resp

    settings.A2_USER_CAN_RESET_PASSWORD = False
    settings.REGISTRATION_OPEN = True
    resp.form.set('username', 'x')
    resp.form.set('password', 'y')
    resp = resp.form.submit(name='login-password-submit')
    assert 'Incorrect Username or password.' in resp
    assert 'use the forgotten password link below' not in resp
    assert 'or create an account.' in resp


def test_login_opened_session_cookie(db, app, settings, simple_user):
    settings.A2_OPENED_SESSION_COOKIE_DOMAIN = 'testserver.local'
    app.cookiejar.clear()
    login(app, simple_user)
    assert 'A2_OPENED_SESSION' in app.cookies

    settings.A2_OPENED_SESSION_COOKIE_SECURE = True
    app.cookiejar.clear()
    login(app, simple_user)
    assert 'A2_OPENED_SESSION' in app.cookies
    for cookie in app.cookiejar:
        if cookie.name == 'A2_OPENED_SESSION':
            assert cookie.secure is True


def test_null_characters(app, db):
    response = app.get('/login/')
    response.form.set('username', 'xx\0xx')
    response.form.set('password', 'whatever')
    response = response.form.submit(name='login-password-submit', status=400)
    assert response.text == 'null character in form data'
