import logging
from unittest import mock

import time_machine
from flask import current_app
from flask import url_for

from canaille.app import models
from canaille.core.endpoints.account import RegistrationPayload


def test_registration_without_email_validation(testclient, backend, foo_group):
    """Tests a nominal registration without email validation."""
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False

    assert not backend.query(models.User, user_name="newuser")
    res = testclient.get(url_for("core.account.registration"), status=200)
    res.form["user_name"] = "newuser"
    res.form["password1"] = "i'm a little pea"
    res.form["password2"] = "i'm a little pea"
    res.form["family_name"] = "newuser"
    res.form["emails-0"] = "newuser@example.test"
    res = res.form.submit()
    assert ("success", "Your account has been created successfully.") in res.flashes

    user = backend.get(models.User, user_name="newuser")
    assert user
    backend.delete(user)


def test_registration_failure_with_different_passwords_and_too_short_password(
    testclient, backend, foo_group
):
    """Tests a nominal registration without email validation but with a wrong confirmation password and a too short password."""
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False

    assert not backend.query(models.User, user_name="newuser")
    res = testclient.get(url_for("core.account.registration"), status=200)
    res.form["user_name"] = "newuser"
    res.form["password1"] = "123"
    res.form["password2"] = "123"
    res.form["family_name"] = "newuser"
    res.form["emails-0"] = "newuser@example.test"
    res = res.form.submit()
    assert ("error", "User account creation failed.") in res.flashes
    res.mustcontain("Field must be at least 8 characters long.")

    res.form["password1"] = "i'm a little pea"
    res.form["password2"] = "i'm not a little pea"
    res = res.form.submit()
    res.mustcontain("Password and confirmation do not match.")
    res.mustcontain('data-percent="100"')

    res.form["password1"] = "i'm a little pea"
    res.form["password2"] = "i'm a little pea"
    res = res.form.submit()
    assert ("success", "Your account has been created successfully.") in res.flashes

    user = backend.get(models.User, user_name="newuser")
    assert user
    backend.delete(user)


def test_registration_with_email_validation(testclient, backend, smtpd, foo_group):
    """Tests a nominal registration with email validation."""
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True

    with time_machine.travel("2020-01-01 02:00:00+00:00", tick=False):
        res = testclient.get(url_for("core.account.join"))
        res.form["email"] = "foo@bar.test"
        res = res.form.submit()

    assert res.flashes == [
        (
            "success",
            "You will receive soon an email to continue the registration process.",
        )
    ]
    assert len(smtpd.messages) == 1

    payload = RegistrationPayload(
        creation_date_isoformat="2020-01-01T02:00:00+00:00",
        user_name="",
        user_name_editable=True,
        email="foo@bar.test",
        groups=[],
    )
    registration_url = url_for(
        "core.account.registration",
        data=payload.b64(),
        hash=payload.build_hash(),
        _external=True,
    )
    text_mail = smtpd.messages[0].get_payload()[0].get_payload(decode=True).decode()
    assert registration_url in text_mail

    assert not backend.query(models.User, user_name="newuser")
    with time_machine.travel("2020-01-01 02:01:00+00:00", tick=False):
        res = testclient.get(registration_url, status=200)
        res.form["user_name"] = "newuser"
        res.form["password1"] = "i'm a little pea"
        res.form["password2"] = "i'm a little pea"
        res.form["family_name"] = "newuser"
        res = res.form.submit()

    assert res.flashes == [
        ("success", "Your account has been created successfully."),
    ]

    user = backend.get(models.User, user_name="newuser")
    assert user
    backend.delete(user)


def test_registration_with_email_already_taken(
    testclient, backend, smtpd, user, foo_group
):
    """Be sure to not leak email existence if HIDE_INVALID_LOGINS is true."""
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True

    testclient.app.config["CANAILLE"]["HIDE_INVALID_LOGINS"] = True
    res = testclient.get(url_for("core.account.join"))
    res.form["email"] = "john@doe.test"
    res = res.form.submit()
    assert res.flashes == [
        (
            "success",
            "You will receive soon an email to continue the registration process.",
        )
    ]

    testclient.app.config["CANAILLE"]["HIDE_INVALID_LOGINS"] = False
    res = testclient.get(url_for("core.account.join"))
    res.form["email"] = "john@doe.test"
    res = res.form.submit()
    assert res.flashes == []
    res.mustcontain("The email 'john@doe.test' is already used")


def test_registration_with_email_validation_needs_a_valid_link(
    testclient, backend, smtpd, foo_group
):
    """Tests a nominal registration without email validation."""
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.get(url_for("core.account.registration"), status=403)


def test_join_page_registration_disabled(testclient, backend, smtpd, foo_group):
    """The join page should not be available if registration is disabled."""
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = False
    testclient.get(url_for("core.account.join"), status=404)


def test_join_page_email_confirmation_disabled(testclient, backend, smtpd, foo_group):
    """The join page should directly redirect to the registration page if email confirmation is disabled."""
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False
    res = testclient.get(url_for("core.account.join"), status=302)
    assert res.location == url_for("core.account.registration")


def test_join_page_already_logged_in(testclient, backend, logged_user, foo_group):
    """The join page should not be accessible for logged users."""
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.get(url_for("core.account.join"), status=403)


@mock.patch("smtplib.SMTP")
def test_registration_mail_error(SMTP, testclient, backend, smtpd, foo_group):
    """Display an error message if the registration mail could not be sent."""
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    SMTP.side_effect = mock.Mock(side_effect=OSError("unit test mail error"))
    res = testclient.get(url_for("core.account.join"))
    res.form["email"] = "foo@bar.test"
    res = res.form.submit(expect_errors=True)

    assert res.flashes == [
        (
            "error",
            "An error happened while sending your registration mail. "
            "Please try again in a few minutes. "
            "If this still happens, please contact the administrators.",
        )
    ]
    assert len(smtpd.messages) == 0


@mock.patch("httpx.get")
def test_registration_with_compromised_password(api_get, testclient, backend):
    """Tests a nominal registration with compromised password."""
    current_app.config["CANAILLE"]["ENABLE_PASSWORD_COMPROMISSION_CHECK"] = True
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False

    # This content simulates a result from the hibp api containing the suffixes of the following password hashes: 'password', '987654321', 'correct horse battery staple', 'zxcvbn123', 'azertyuiop123'
    class Response:
        content = b"1E4C9B93F3F0682250B6CF8331B7EE68FD8:3\r\nCAA6D483CC3887DCE9D1B8EB91408F1EA7A:3\r\nAD6438836DBE526AA231ABDE2D0EEF74D42:3\r\n8289894DDB6317178960AB5AE98B81BBF97:1\r\n5FF0B6F9EAC40D5CA7B4DAA7B64F0E6F4AA:2\r\n"

    api_get.return_value = Response

    assert not backend.query(models.User, user_name="newuser")
    res = testclient.get(url_for("core.account.registration"), status=200)
    res.form["user_name"] = "newuser"
    res.form["password1"] = "987654321"
    res.form["password2"] = "987654321"
    res.form["family_name"] = "newuser"
    res.form["emails-0"] = "newuser@example.test"
    res = res.form.submit()
    res.mustcontain(
        "This password appears on public compromission databases and is not secure."
    )

    user = backend.get(models.User, user_name="newuser")
    assert user is None


@mock.patch("httpx.get")
def test_registration_with_compromised_password_request_api_failed_but_account_created(
    api_get, testclient, backend, caplog
):
    current_app.config["CANAILLE"]["ENABLE_PASSWORD_COMPROMISSION_CHECK"] = True
    api_get.side_effect = mock.Mock(side_effect=Exception())
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False

    assert not backend.query(models.User, user_name="newuser")

    res = testclient.get(url_for("core.account.registration"), status=200)
    res.form["user_name"] = "newuser"
    res.form["password1"] = "123456789"
    res.form["password2"] = "123456789"
    res.form["family_name"] = "newuser"
    res.form["emails-0"] = "newuser@example.test"

    res = res.form.submit()

    assert (
        "canaille",
        logging.ERROR,
        "Password compromise investigation failed on HIBP API.",
    ) in caplog.record_tuples
    assert (
        "error",
        "Password compromise investigation failed. Please contact the administrators.",
    ) in res.flashes
    assert ("success", "Your account has been created successfully.") in res.flashes

    user = backend.get(models.User, user_name="newuser")
    assert user
    backend.delete(user)


@mock.patch("httpx.get")
def test_compromised_password_validator_with_failure_of_api_request_and_success_mail_to_admin_from_register_form(
    api_get, testclient, backend, caplog, smtpd
):
    current_app.config["CANAILLE"]["ENABLE_PASSWORD_COMPROMISSION_CHECK"] = True
    api_get.side_effect = mock.Mock(side_effect=Exception())
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False

    assert not backend.query(models.User, user_name="newuser")

    res = testclient.get(url_for("core.account.registration"), status=200)
    res.form["user_name"] = "newuser"
    res.form["password1"] = "123456789"
    res.form["password2"] = "123456789"
    res.form["family_name"] = "newuser"
    res.form["emails-0"] = "newuser@example.test"

    res = res.form.submit()

    assert (
        "canaille",
        logging.ERROR,
        "Password compromise investigation failed on HIBP API.",
    ) in caplog.record_tuples
    assert (
        "error",
        "Password compromise investigation failed. Please contact the administrators.",
    ) in res.flashes
    assert (
        "info",
        "We have informed your administrator about the failure of the password compromise investigation.",
    ) in res.flashes
    assert ("success", "Your account has been created successfully.") in res.flashes
    assert len(smtpd.messages) == 1

    user = backend.get(models.User, user_name="newuser")
    assert user
    backend.delete(user)


@mock.patch("httpx.get")
def test_compromised_password_validator_with_failure_of_api_request_and_fail_to_send_mail_to_admin_from_register_form(
    api_get, testclient, backend, caplog
):
    current_app.config["CANAILLE"]["ENABLE_PASSWORD_COMPROMISSION_CHECK"] = True
    api_get.side_effect = mock.Mock(side_effect=Exception())
    current_app.config["CANAILLE"]["SMTP"]["TLS"] = False
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False

    assert not backend.query(models.User, user_name="newuser")

    res = testclient.get(url_for("core.account.registration"), status=200)
    res.form["user_name"] = "newuser"
    res.form["password1"] = "123456789"
    res.form["password2"] = "123456789"
    res.form["family_name"] = "newuser"
    res.form["emails-0"] = "newuser@example.test"

    res = res.form.submit()

    assert (
        "canaille",
        logging.ERROR,
        "Password compromise investigation failed on HIBP API.",
    ) in caplog.record_tuples
    assert (
        "error",
        "Password compromise investigation failed. Please contact the administrators.",
    ) in res.flashes
    assert (
        "error",
        "An error occurred while communicating the incident to the administrators. "
        "Please update your password as soon as possible. "
        "If this still happens, please contact the administrators.",
    ) in res.flashes
    assert ("success", "Your account has been created successfully.") in res.flashes

    user = backend.get(models.User, user_name="newuser")
    assert user
    backend.delete(user)


@mock.patch("httpx.get")
def test_compromised_password_validator_with_failure_of_api_request_without_smtp_from_register_form(
    api_get, testclient, backend, caplog
):
    current_app.config["CANAILLE"]["ENABLE_PASSWORD_COMPROMISSION_CHECK"] = True
    api_get.side_effect = mock.Mock(side_effect=Exception())
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False

    assert not backend.query(models.User, user_name="newuser")

    current_app.config["CANAILLE"]["SMTP"] = None

    res = testclient.get(url_for("core.account.registration"), status=200)
    res.form["user_name"] = "newuser"
    res.form["password1"] = "123456789"
    res.form["password2"] = "123456789"
    res.form["family_name"] = "newuser"
    res.form["emails-0"] = "newuser@example.test"

    res = res.form.submit()

    assert (
        "canaille",
        logging.ERROR,
        "Password compromise investigation failed on HIBP API.",
    ) in caplog.record_tuples
    assert (
        "error",
        "Password compromise investigation failed. Please contact the administrators.",
    ) not in res.flashes

    user = backend.get(models.User, user_name="newuser")
    assert user
    backend.delete(user)


@mock.patch("httpx.get")
def test_compromised_password_validator_with_failure_of_api_request_without_admin_email_from_register_form(
    api_get, testclient, backend, caplog
):
    current_app.config["CANAILLE"]["ENABLE_PASSWORD_COMPROMISSION_CHECK"] = True
    api_get.side_effect = mock.Mock(side_effect=Exception())
    testclient.app.config["CANAILLE"]["ENABLE_REGISTRATION"] = True
    testclient.app.config["CANAILLE"]["EMAIL_CONFIRMATION"] = False

    assert not backend.query(models.User, user_name="newuser")

    current_app.config["CANAILLE"]["ADMIN_EMAIL"] = None

    res = testclient.get(url_for("core.account.registration"), status=200)
    res.form["user_name"] = "newuser"
    res.form["password1"] = "123456789"
    res.form["password2"] = "123456789"
    res.form["family_name"] = "newuser"
    res.form["emails-0"] = "newuser@example.test"

    res = res.form.submit()

    assert (
        "canaille",
        logging.ERROR,
        "Password compromise investigation failed on HIBP API.",
    ) in caplog.record_tuples
    assert (
        "error",
        "Password compromise investigation failed. Please contact the administrators.",
    ) not in res.flashes

    user = backend.get(models.User, user_name="newuser")
    assert user
    backend.delete(user)
