"""
Unit tests for the mailbox API
"""
# pylint: disable=W0613, C0302

import json
import logging
import re
from logging import Logger
from unittest import mock

from django.test.utils import override_settings

import pytest
import responses
from requests.exceptions import HTTPError
from rest_framework import status
from rest_framework.test import APIClient

from core import factories as core_factories

from mailbox_manager import enums, factories, models
from mailbox_manager.api.client import serializers
from mailbox_manager.tests.fixtures import dimail as dimail_responses

logger = logging.getLogger(__name__)
pytestmark = pytest.mark.django_db


def test_api_mailboxes__create_anonymous_forbidden(mailbox_data):
    """Anonymous users should not be able to create a new mailbox via the API."""
    mail_domain = factories.MailDomainEnabledFactory()
    response = APIClient().post(
        f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
        mailbox_data,
    )
    assert response.status_code == status.HTTP_401_UNAUTHORIZED
    assert not models.Mailbox.objects.exists()


@responses.activate
def test_api_mailboxes__create_no_access_forbidden_not_found(mailbox_data):
    """Authenticated users should not be able to create mailbox
    without specific role on mail domain."""
    mail_domain = factories.MailDomainEnabledFactory()

    client = APIClient()
    client.force_login(core_factories.UserFactory())
    # We add no simulated response in Responses
    # because we expect no "outside" calls to be made
    response = client.post(
        f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )

    assert response.status_code == status.HTTP_404_NOT_FOUND
    assert not models.Mailbox.objects.exists()


def test_api_mailboxes__create_viewer_forbidden(mailbox_data):
    """Users with viewer role should not be able to create mailbox on the mail domain."""
    mail_domain = factories.MailDomainEnabledFactory()
    access = factories.MailDomainAccessFactory(
        role=enums.MailDomainRoleChoices.VIEWER, domain=mail_domain
    )

    client = APIClient()
    client.force_login(access.user)
    response = client.post(
        f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )

    assert response.status_code == status.HTTP_403_FORBIDDEN
    assert not models.Mailbox.objects.exists()


def test_api_mailboxes__create_display_name_must_be_unique():
    """Primary id on OpenXchange is display name (first_name + last_name).
    It needs to be unique on each context. We don't track context info for now
    but can impose case-insensitive uniqueness by domain."""
    access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)
    existing_mailbox = factories.MailboxFactory(domain=access.domain)

    client = APIClient()
    client.force_login(access.user)

    new_mailbox_data = {
        "local_part": "something_else",
        "first_name": existing_mailbox.first_name.upper(),  # checks case-insensitivity
        "last_name": existing_mailbox.last_name.lower(),
    }
    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        new_mailbox_data,
        format="json",
    )
    assert response.status_code == status.HTTP_400_BAD_REQUEST
    assert response.json() == {
        "__all__": [
            "Mailbox with this First name, Last name and Domain already exists.",
        ]
    }
    assert models.Mailbox.objects.count() == 1


@responses.activate
def test_api_mailboxes__create_display_name_no_constraint_on_different_domains(
    dimail_token_ok,
):
    """Should still be allowed to user same display name on another domain"""
    existing_mailbox = factories.MailboxFactory()

    access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.ADMIN)
    new_mailbox_data = {
        "local_part": "something_else",
        "first_name": existing_mailbox.first_name,
        "last_name": existing_mailbox.last_name,
    }

    # ensure response
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body=dimail_responses.response_mailbox_created(
            f"{new_mailbox_data['local_part']}@{access.domain.name}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )

    client = APIClient()
    client.force_login(access.user)
    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        new_mailbox_data,
        format="json",
    )
    assert response.status_code == status.HTTP_201_CREATED
    assert response.json()["first_name"] == existing_mailbox.first_name
    assert response.json()["last_name"] == existing_mailbox.last_name
    assert response.json()["status"] == enums.MailboxStatusChoices.ENABLED
    assert models.Mailbox.objects.count() == 2


@responses.activate
@pytest.mark.parametrize(
    "role",
    [enums.MailDomainRoleChoices.OWNER, enums.MailDomainRoleChoices.ADMIN],
)
def test_api_mailboxes__create_roles_success(role, dimail_token_ok, mailbox_data):
    """Users with owner or admin role should be able to create mailbox on the mail domain."""
    mail_domain = factories.MailDomainEnabledFactory()
    access = factories.MailDomainAccessFactory(role=role, domain=mail_domain)

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(
            rf".*/domains/{mail_domain.name}/mailboxes/{mailbox_data['local_part']}$"
        ),
        body=dimail_responses.response_mailbox_created(
            f"{mailbox_data['local_part']}@{mail_domain.name}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )
    dimail_responses.response_login_code_ok(
        mail_domain.name, mailbox_data["local_part"]
    )
    response = client.post(
        f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )

    assert response.status_code == status.HTTP_201_CREATED
    mailbox = models.Mailbox.objects.get()

    assert mailbox.local_part == mailbox_data["local_part"]
    assert mailbox.secondary_email == mailbox_data["secondary_email"]
    assert response.json() == {
        "id": str(mailbox.id),
        "first_name": str(mailbox.first_name),
        "last_name": str(mailbox.last_name),
        "local_part": str(mailbox.local_part),
        "secondary_email": str(mailbox.secondary_email),
        "status": enums.MailboxStatusChoices.ENABLED,
    }


@responses.activate
@pytest.mark.parametrize(
    "role",
    [enums.MailDomainRoleChoices.OWNER, enums.MailDomainRoleChoices.ADMIN],
)
def test_api_mailboxes__create_with_accent_success(role, dimail_token_ok):
    """Users with proper abilities should be able to create mailbox on the mail domain with a
    first_name accentuated."""
    mail_domain = factories.MailDomainEnabledFactory()
    access = factories.MailDomainAccessFactory(role=role, domain=mail_domain)

    client = APIClient()
    client.force_login(access.user)

    mailbox_values = serializers.MailboxSerializer(
        factories.MailboxFactory.build(first_name="Aimé")
    ).data

    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{mail_domain.name}/mailboxes/"),
        body=dimail_responses.response_mailbox_created(
            f"{mailbox_values['local_part']}@{mail_domain.name}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )
    dimail_responses.response_login_code_ok(mail_domain, mailbox_values["local_part"])
    response = client.post(
        f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
        mailbox_values,
        format="json",
    )
    assert response.status_code == status.HTTP_201_CREATED
    mailbox = models.Mailbox.objects.get()

    assert mailbox.local_part == mailbox_values["local_part"]
    assert mailbox.secondary_email == mailbox_values["secondary_email"]
    assert response.json() == {
        "id": str(mailbox.id),
        "first_name": str(mailbox.first_name),
        "last_name": str(mailbox.last_name),
        "local_part": str(mailbox.local_part),
        "secondary_email": str(mailbox.secondary_email),
        "status": enums.MailboxStatusChoices.ENABLED,
    }


@responses.activate
def test_api_mailboxes__create_lowercase(dimail_token_ok, mailbox_data):
    """Should lowercase emails on creation."""
    access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.ADMIN)
    mailbox_data["local_part"] = "WeiRdCaSe"

    # ensure response
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(
            rf".*/domains/{access.domain.name}/mailboxes/{mailbox_data['local_part'].lower()}"
        ),
        body=dimail_responses.response_mailbox_created(
            f"{mailbox_data['local_part']}@{access.domain.name}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )

    dimail_responses.response_login_code_ok(
        access.domain, mailbox_data["local_part"].lower()
    )
    client = APIClient()
    client.force_login(access.user)
    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )
    assert response.status_code == status.HTTP_201_CREATED
    assert response.json()["local_part"] == mailbox_data["local_part"].lower()


def test_api_mailboxes__create_administrator_missing_fields():
    """
    Administrator users should not be able to create mailboxes
    without local part.
    """
    mail_domain = factories.MailDomainEnabledFactory()
    access = factories.MailDomainAccessFactory(
        role=enums.MailDomainRoleChoices.ADMIN, domain=mail_domain
    )
    client = APIClient()
    client.force_login(access.user)

    mailbox_values = serializers.MailboxSerializer(
        factories.MailboxFactory.build()
    ).data
    del mailbox_values["local_part"]
    response = client.post(
        f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
        mailbox_values,
        format="json",
    )
    assert response.status_code == status.HTTP_400_BAD_REQUEST
    assert not models.Mailbox.objects.exists()
    assert response.json() == {"local_part": ["This field is required."]}


@responses.activate
@pytest.mark.parametrize(
    "role",
    [enums.MailDomainRoleChoices.OWNER, enums.MailDomainRoleChoices.ADMIN],
)
def test_api_mailboxes__create_without_secondary_email(role, caplog, dimail_token_ok):
    """
    Creating a new mailbox should not require a secondary email.
    We should be able to create a mailbox but not send any email notification.
    """
    mail_domain = factories.MailDomainEnabledFactory()
    access = factories.MailDomainAccessFactory(role=role, domain=mail_domain)
    client = APIClient()
    client.force_login(access.user)
    mailbox_values = serializers.MailboxSerializer(
        factories.MailboxFactory.build()
    ).data
    del mailbox_values["secondary_email"]

    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{mail_domain.name}/mailboxes/"),
        body=dimail_responses.response_mailbox_created(
            f"{mailbox_values['local_part']}@{mail_domain.name}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )
    response = client.post(
        f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
        mailbox_values,
        format="json",
    )
    assert response.status_code == status.HTTP_201_CREATED
    mailbox = models.Mailbox.objects.get()
    assert (
        caplog.records[0].message
        == f"Email notification for {mailbox} creation not sent "
        "because no secondary email found"
    )


@pytest.mark.parametrize(
    "role",
    [
        enums.MailDomainRoleChoices.OWNER,
        enums.MailDomainRoleChoices.ADMIN,
    ],
)
def test_api_mailboxes__cannot_create_on_disabled_domain(role, mailbox_data):
    """Admin and owner users should not be able to create mailboxes for a disabled domain"""
    mail_domain = factories.MailDomainFactory(
        status=enums.MailDomainStatusChoices.DISABLED
    )
    access = factories.MailDomainAccessFactory(role=role, domain=mail_domain)

    client = APIClient()
    client.force_login(access.user)
    response = client.post(
        f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )
    assert response.status_code == status.HTTP_400_BAD_REQUEST
    assert not models.Mailbox.objects.exists()
    assert response.json() == [
        "You can't create or update a mailbox for a disabled domain."
    ]


@responses.activate
def test_api_mailboxes__no_dimail_call_if_mailbox_creation_failed():
    """Duplication case fails on our side at creation step thanks to unique_together
    on Mailbox model and no dimail call should be made."""
    mail_domain = factories.MailDomainEnabledFactory()
    mailbox = factories.MailboxFactory(domain=mail_domain)
    access = factories.MailDomainAccessFactory(
        role=enums.MailDomainRoleChoices.ADMIN, domain=mail_domain
    )

    client = APIClient()
    client.force_login(access.user)

    # now we try to make the same mailbox
    mailbox_data = serializers.MailboxSerializer(mailbox).data
    with responses.RequestsMock() as rsps:
        # no call expected

        response = client.post(
            f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
            mailbox_data,
            format="json",
        )
        # try to create duplicate mailbox but django validation does not agree
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        # no new mailbox should be created
        assert models.Mailbox.objects.count() == 1
        # no dimail call should be made
        assert len(rsps.calls) == 0


@responses.activate
def test_api_mailboxes__same_local_part_on_different_domains(dimail_token_ok):
    """A domain admin should be able to create a mailbox with the same local part
    of another mailbox, on different domain."""
    # a mailbox exists on another domain
    existing_mailbox = factories.MailboxFactory()

    access = factories.MailDomainAccessFactory(
        role=enums.MailDomainRoleChoices.ADMIN,
        domain=factories.MailDomainEnabledFactory(),
    )
    client = APIClient()
    client.force_login(access.user)

    # create a full dict with same local part as another existing mailbox
    mailbox_values = serializers.MailboxSerializer(
        factories.MailboxFactory.build(local_part=existing_mailbox.local_part)
    ).data

    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body=dimail_responses.response_mailbox_created(
            f"{mailbox_values['local_part']}@{access.domain.name}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )
    dimail_responses.response_login_code_ok(access.domain, existing_mailbox.local_part)
    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_values,
        format="json",
    )

    assert response.status_code == status.HTTP_201_CREATED
    assert (
        len(models.Mailbox.objects.filter(local_part=existing_mailbox.local_part)) == 2
    )


@pytest.mark.parametrize(
    "domain_status",
    [
        enums.MailDomainStatusChoices.PENDING,
        enums.MailDomainStatusChoices.FAILED,
    ],
)
def test_api_mailboxes__create_pending_mailboxes(domain_status, mailbox_data):
    """
    Admin and owner users should be able to create mailboxes, including on pending and failed
    domains.
    Mailboxes created on pending and failed domains should have the "pending" status
    """
    mail_domain = factories.MailDomainFactory(status=domain_status)
    access = factories.MailDomainAccessFactory(
        role=enums.MailDomainRoleChoices.ADMIN, domain=mail_domain
    )

    client = APIClient()
    client.force_login(access.user)
    # We add no response in RequestsMock
    # because we expect no outside calls to be made
    response = client.post(
        f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )
    assert response.status_code == status.HTTP_201_CREATED
    mailbox = models.Mailbox.objects.get()
    assert mailbox.status == "pending"


@responses.activate
def test_api_mailboxes__no_conflict_existing_alias_ok(mailbox_data, dimail_token_ok):
    """Can create mailbox even if local_part is already used by an alias."""
    alias = factories.AliasFactory()
    access = factories.MailDomainAccessFactory(
        role=enums.MailDomainRoleChoices.ADMIN, domain=alias.domain
    )

    client = APIClient()
    client.force_login(access.user)

    responses.post(
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body=dimail_responses.response_mailbox_created(
            f"{alias.local_part}@{access.domain.name}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )
    dimail_responses.response_login_code_ok(alias.domain, alias.local_part)

    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        {
            "local_part": alias.local_part,
            "first_name": mailbox_data["first_name"],
            "last_name": mailbox_data["last_name"],
            "secondary_email": mailbox_data["secondary_email"],
        },
        format="json",
    )
    assert response.status_code == status.HTTP_201_CREATED
    assert models.Mailbox.objects.exists()


### REACTING TO DIMAIL-API
### We mock dimail's responses to avoid testing dimail's container too
@responses.activate
def test_api_mailboxes__domain_viewer_provisioning_api_not_called(mailbox_data):
    """
    Provisioning API should not be called if a domain viewer tries
    to create a mailbox on a domain they are not owner/admin of.
    """
    access = factories.MailDomainAccessFactory(
        domain=factories.MailDomainEnabledFactory(),
        user=core_factories.UserFactory(),
        role=enums.MailDomainRoleChoices.VIEWER,
    )

    client = APIClient()
    client.force_login(access.user)
    # We add no simulated response in RequestsMock
    # because we expected no "outside" calls to be made
    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )
    # No exception raised by RequestsMock means no call was sent
    # our API blocked the request before sending it
    assert response.status_code == status.HTTP_403_FORBIDDEN


@responses.activate
@mock.patch.object(Logger, "error")
def test_api_mailboxes__async_dimail_unauthorized(
    mock_error, dimail_token_ok, mailbox_data
):
    """
    Dimail should raise an error if token has been successfully granted
    but mailbox creation request returns a 403.
    i.e. user exists on dimail-api but has no permission on that domain
    """
    # creating all needed objects

    # this access somehow exists solely in our database but not in dimail
    access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(
            rf".*/domains/{access.domain.name}/mailboxes/{mailbox_data['local_part']}"
        ),
        status=status.HTTP_403_FORBIDDEN,
        content_type="application/json",
    )

    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )
    assert response.status_code == status.HTTP_403_FORBIDDEN

    assert mock_error.call_count == 1
    assert mock_error.call_args_list[0][0] == (
        "[DIMAIL] 403 Forbidden: you cannot access domain %s",
        access.domain.name,
    )


@responses.activate
@pytest.mark.parametrize(
    "role",
    [enums.MailDomainRoleChoices.ADMIN, enums.MailDomainRoleChoices.OWNER],
)
def test_api_mailboxes__domain_owner_or_admin_successful_creation_and_provisioning(
    dimail_token_ok, role, mailbox_data
):
    """
    Domain owner/admin should be able to create mailboxes.
    Provisioning API should be called when owner/admin makes a call.
    successful 201 response from dimail should trigger mailbox creation on our side.
    """
    access = factories.MailDomainAccessFactory(role=role)

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body=dimail_responses.response_mailbox_created(
            f"{mailbox_data['local_part']}@{access.domain.name}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )
    dimail_responses.response_login_code_ok(access.domain, mailbox_data["local_part"])
    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )

    # Checks payload sent to email-provisioning API
    payload = json.loads(responses.calls[1].request.body)
    assert payload == {
        "displayName": f"{mailbox_data['first_name']} {mailbox_data['last_name']}",
        "givenName": mailbox_data["first_name"],
        "surName": mailbox_data["last_name"],
    }

    # Checks response
    assert response.status_code == status.HTTP_201_CREATED
    mailbox = models.Mailbox.objects.get()
    assert response.json() == {
        "id": str(mailbox.id),
        "first_name": str(mailbox_data["first_name"]),
        "last_name": str(mailbox_data["last_name"]),
        "local_part": str(mailbox_data["local_part"]),
        "secondary_email": str(mailbox_data["secondary_email"]),
        "status": enums.MailboxStatusChoices.ENABLED,
    }
    assert mailbox.first_name == mailbox_data["first_name"]
    assert mailbox.last_name == mailbox_data["last_name"]
    assert mailbox.local_part == mailbox_data["local_part"]
    assert mailbox.secondary_email == mailbox_data["secondary_email"]


@responses.activate
@pytest.mark.parametrize(
    "role",
    [enums.MailDomainRoleChoices.ADMIN, enums.MailDomainRoleChoices.OWNER],
)
def test_api_mailboxes__domain_owner_or_admin_successful_creation_sets_password(
    role, mailbox_data, dimail_token_ok
):
    """
    Upon successful creation, mailbox password should be synced to Dimail's.
    """
    # creating all needed objects
    access = factories.MailDomainAccessFactory(role=role)

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body=dimail_responses.response_mailbox_created(
            f"{mailbox_data['local_part']}@{access.domain.name}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )
    dimail_responses.response_login_code_ok(access.domain, mailbox_data["local_part"])
    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )

    # Checks payload sent to email-provisioning API
    payload = json.loads(responses.calls[1].request.body)
    assert payload == {
        "displayName": f"{mailbox_data['first_name']} {mailbox_data['last_name']}",
        "givenName": mailbox_data["first_name"],
        "surName": mailbox_data["last_name"],
    }

    # Checks response
    assert response.status_code == status.HTTP_201_CREATED
    mailbox = models.Mailbox.objects.get()
    assert mailbox.check_password("password")


@responses.activate
@override_settings(MAIL_PROVISIONING_API_CREDENTIALS="wrongCredentials")
def test_api_mailboxes__dimail_token_permission_denied(caplog, mailbox_data):
    """
    API should raise a clear "permission denied" error
    when receiving a permission denied from dimail upon requesting token.
    """
    # creating all needed objects
    access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    responses.add(
        responses.GET,
        re.compile(r".*/token/"),
        body='{"details": "Permission denied"}',
        status=status.HTTP_403_FORBIDDEN,
        content_type="application/json",
    )

    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )

    assert response.status_code == status.HTTP_403_FORBIDDEN
    assert response.json() == {
        "detail": "Token denied. Please check your MAIL_PROVISIONING_API_CREDENTIALS."
    }
    # mailbox was created in our side only and in pending status
    mailbox = models.Mailbox.objects.get()
    assert mailbox.status == enums.MailboxStatusChoices.PENDING

    # Check error logger was called
    log_messages = [msg.message for msg in caplog.records]
    assert (
        "[DIMAIL] 403 Forbidden: Could not retrieve a token,\
please check 'MAIL_PROVISIONING_API_CREDENTIALS' setting."
        in log_messages
    )


@responses.activate
def test_api_mailboxes__user_unrelated_to_domain(dimail_token_ok, mailbox_data):
    """
    API should raise a clear "permission denied" when dimail returns a permission denied
    on mailbox creation. This means token was granted for this user
    but user is not allowed to modify this domain (i.e. does not have a allow)
    """
    # creating all needed objects
    access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body='{"details": "Permission denied"}',
        status=status.HTTP_403_FORBIDDEN,
        content_type="application/json",
    )

    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )

    assert response.status_code == status.HTTP_403_FORBIDDEN
    assert response.json() == {
        "detail": "Permission denied. Please check your MAIL_PROVISIONING_API_CREDENTIALS."
    }
    # mailbox was created in our side only and in pending status
    mailbox = models.Mailbox.objects.get()
    assert mailbox.status == enums.MailboxStatusChoices.PENDING


@responses.activate
def test_api_mailboxes__duplicate_display_name(dimail_token_ok, mailbox_data):
    """
    In OpenXchange, the display name (first + last name) must be unique.
    In absence of a specific response from dimail (yet), we catch errors 500 and
    check if it's not due to the display name already existing in the context
    """
    access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body='{"detail": "Internal server error"}',
        status=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content_type="application/json",
    )
    responses.add(
        responses.GET,
        re.compile(
            rf".*/domains/{access.domain.name}/address/{mailbox_data['local_part']}"
        ),
        body=json.dumps(
            {
                "domain_name": str(access.domain),
                "user_name": mailbox_data["local_part"],
                "is_alias": "false",
                "is_mailbox": "false",
                "is_ox": "true",
                "ox_primary_email": f"{mailbox_data['local_part']}@some_other_domain.com",
                "ox_senders": [
                    "some-alias@un-domaine.fr",
                    f"{mailbox_data['local_part']}@some_third_domain.com",
                ],
            }
        ),
        status=status.HTTP_200_OK,
        content_type="application/json",
    )

    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )
    assert response.status_code == status.HTTP_400_BAD_REQUEST
    assert response.json() == {
        "NON_FIELD_ERRORS": [
            f"First name + last name combination already in use in this context : \
{mailbox_data['local_part']}@some_other_domain.com."
        ]
    }


@responses.activate
def test_api_mailboxes__handling_dimail_unexpected_error(
    dimail_token_ok, caplog, mailbox_data
):
    """
    API should raise a clear error when dimail returns an unexpected response.
    """
    access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body='{"detail": "Internal server error"}',
        status=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content_type="application/json",
    )
    responses.add(
        responses.GET,
        re.compile(
            rf".*/domains/{access.domain.name}/address/{mailbox_data['local_part']}/"
        ),
        body='{"detail": "Domain not found"}',
        status=status.HTTP_404_NOT_FOUND,
        content_type="application/json",
    )

    with pytest.raises(HTTPError):
        response = client.post(
            f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
            mailbox_data,
            format="json",
        )
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
        assert response.json() == {
            "detail": "Unexpected response from dimail: {'details': 'Internal server error'}"
        }

    # mailbox was created in our side only and in pending status
    mailbox = models.Mailbox.objects.get()
    assert mailbox.status == enums.MailboxStatusChoices.PENDING

    # Check error logger was called
    assert caplog.records[0].levelname == "ERROR"
    assert (
        caplog.records[0].message
        == "[DIMAIL] unexpected error: 500 {'detail': 'Internal server error'}"
    )


@responses.activate
def test_api_mailboxes__display_name_duplicate_error(dimail_token_ok, mailbox_data):
    """
    API should raise a clear error when display_name is already used on context.
    """
    access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body='{"detail": "Internal server error"}',
        status=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content_type="application/json",
    )
    responses.add(
        responses.GET,
        re.compile(
            rf".*/domains/{access.domain.name}/address/{mailbox_data['local_part']}/"
        ),
        body=json.dumps(
            {
                "domain_name": access.domain.name,
                "user_name": mailbox_data["local_part"],
                "is_alias": False,
                "is_mailbox": False,
                "is_ox": True,
                "ox_primary_email": f"{mailbox_data['local_part']}@primary.domain.com",
                "ox_senders": [
                    f"{mailbox_data['local_part']}@secondary.domain.com",
                ],
            }
        ),
        status=status.HTTP_200_OK,
        content_type="application/json",
    )

    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )
    assert response.status_code == status.HTTP_400_BAD_REQUEST
    assert response.json() == {
        "NON_FIELD_ERRORS": [
            f"First name + last name combination already in use in this \
context : {mailbox_data['local_part']}@primary.domain.com."
        ]
    }
    assert not models.Mailbox.objects.exists()  # failing mailbox was not created


@responses.activate
@mock.patch.object(Logger, "error")
@mock.patch.object(Logger, "info")
def test_api_mailboxes__send_correct_logger_infos(
    mock_info, mock_error, dimail_token_ok, mailbox_data
):
    """
    Upon requesting mailbox creation, logs should report request user.
    """
    access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body=dimail_responses.response_mailbox_created(
            f"{mailbox_data['local_part']}@{access.domain.name}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )
    dimail_responses.response_login_code_ok(access.domain, mailbox_data["local_part"])
    response = client.post(
        f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
        mailbox_data,
        format="json",
    )
    assert response.status_code == status.HTTP_201_CREATED

    # Logger
    assert not mock_error.called
    # Check all expected log messages are present, order doesn't matter
    expected_messages = {
        ("Token successfully granted by mail-provisioning API.",),
        (
            "Mailbox successfully created on domain %s by user %s",
            str(access.domain),
            access.user.sub,
        ),
    }
    actual_messages = {args for args, _ in mock_info.call_args_list}
    assert expected_messages.issubset(actual_messages)


@responses.activate
@mock.patch.object(Logger, "info")
def test_api_mailboxes__sends_new_mailbox_notification(
    mock_info, dimail_token_ok, mailbox_data
):
    """
    Creating a new mailbox should send confirmation email
    to secondary email.
    """

    access = factories.MailDomainAccessFactory(
        user=core_factories.UserFactory(language="en-us"),
        role=enums.MailDomainRoleChoices.OWNER,
    )

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body=dimail_responses.response_mailbox_created(
            f"{mailbox_data['local_part']}@{access.domain}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )
    dimail_responses.response_login_code_ok(access.domain, mailbox_data["local_part"])

    with mock.patch("django.core.mail.send_mail") as mock_send:
        client.post(
            f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
            mailbox_data,
            format="json",
        )

    assert mock_send.call_count == 1
    assert "Your new mailbox information" in mock_send.mock_calls[0][1][1]
    assert "OneTimeCode" in mock_send.mock_calls[0][1][1]
    assert "password" not in mock_send.mock_calls[0][1][1]
    assert mock_send.mock_calls[0][1][3][0] == mailbox_data["secondary_email"]

    expected_messages = {
        (
            "Information for mailbox %s sent to %s.",
            f"{mailbox_data['local_part']}@{access.domain.name}",
            mailbox_data["secondary_email"],
        )
    }
    actual_messages = {args for args, _ in mock_info.call_args_list}
    assert expected_messages.issubset(actual_messages)


@responses.activate
@override_settings(SEND_MAILBOX_PASSWORD=True)
@mock.patch.object(Logger, "info")
def test_api_mailboxes__sends_new_mailbox_notification_with_password(
    mock_info, dimail_token_ok, mailbox_data
):
    """
    Creating a new mailbox should send confirmation email
    to secondary email.
    """

    access = factories.MailDomainAccessFactory(
        user=core_factories.UserFactory(language="en-us"),
        role=enums.MailDomainRoleChoices.OWNER,
    )

    client = APIClient()
    client.force_login(access.user)
    # Ensure successful response using "responses":
    # token response in fixtures
    responses.add(
        responses.POST,
        re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
        body=dimail_responses.response_mailbox_created(
            f"{mailbox_data['local_part']}@{access.domain}"
        ),
        status=status.HTTP_201_CREATED,
        content_type="application/json",
    )
    dimail_responses.response_login_code_ok(access.domain, mailbox_data["local_part"])

    with mock.patch("django.core.mail.send_mail") as mock_send:
        client.post(
            f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
            mailbox_data,
            format="json",
        )

    assert mock_send.call_count == 1
    assert "Your new mailbox information" in mock_send.mock_calls[0][1][1]
    assert "OneTimeCode" in mock_send.mock_calls[0][1][1]
    assert "password" in mock_send.mock_calls[0][1][1]
    assert mock_send.mock_calls[0][1][3][0] == mailbox_data["secondary_email"]

    expected_messages = {
        (
            "Information for mailbox %s sent to %s.",
            f"{mailbox_data['local_part']}@{access.domain.name}",
            mailbox_data["secondary_email"],
        )
    }
    actual_messages = {args for args, _ in mock_info.call_args_list}
    assert expected_messages.issubset(actual_messages)
