"""Test the MailboxViewSet."""

from django.test import override_settings
from django.urls import reverse
from django.utils import timezone

import pytest
from rest_framework import status
from rest_framework.test import APIClient

from core import enums, factories, models

pytestmark = pytest.mark.django_db


@pytest.fixture(name="user")
def fixture_user():
    """Create a test user."""
    return factories.UserFactory()


@pytest.fixture(name="mailbox")
def fixture_mailbox():
    """Create a test mailbox."""
    return factories.MailboxFactory()


@pytest.fixture(name="superuser")
def fixture_superuser():
    """Create a test superuser."""
    return factories.UserFactory(is_superuser=True, is_staff=True)


@pytest.mark.django_db
class TestMailboxViewSet:
    """Test the MailboxViewSet."""

    def test_list(self):
        """Test the list method."""
        # Create authenticated user with access to 2 mailboxes
        authenticated_user = factories.UserFactory()
        user_mailbox1 = factories.MailboxFactory()
        user_mailbox2 = factories.MailboxFactory()
        other_mailbox = factories.MailboxFactory()
        # Authenticated user has access to 2 mailboxes
        factories.MailboxAccessFactory(
            mailbox=user_mailbox1,
            user=authenticated_user,
            role=models.MailboxRoleChoices.VIEWER,
        )

        factories.MailboxAccessFactory(
            mailbox=user_mailbox2,
            user=authenticated_user,
            role=models.MailboxRoleChoices.EDITOR,
        )
        # Create an other user with access to other mailbox
        other_user = factories.UserFactory()
        factories.MailboxAccessFactory(
            mailbox=other_mailbox,
            user=other_user,
            role=models.MailboxRoleChoices.EDITOR,
        )

        # create a thread with one unread message for user_mailbox1 (with delivering)
        thread1 = factories.ThreadFactory(
            has_delivery_pending=True,
            messaged_at=timezone.now(),
            has_active=True,
            active_messaged_at=timezone.now(),
        )
        factories.ThreadAccessFactory(
            mailbox=user_mailbox1,
            thread=thread1,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        factories.MessageFactory(thread=thread1)

        # create a thread with one read message for user_mailbox2 (with delivering)
        thread2 = factories.ThreadFactory(
            has_delivery_pending=True, messaged_at=timezone.now()
        )
        factories.ThreadAccessFactory(
            mailbox=user_mailbox2,
            thread=thread2,
            role=enums.ThreadAccessRoleChoices.EDITOR,
            read_at=timezone.now(),
        )
        factories.MessageFactory(thread=thread2)

        # create a thread with one unread message for user_mailbox2 (no delivering)
        thread3 = factories.ThreadFactory(
            has_delivery_pending=False,
            messaged_at=timezone.now(),
            has_active=True,
            active_messaged_at=timezone.now(),
        )
        factories.ThreadAccessFactory(
            mailbox=user_mailbox2,
            thread=thread3,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        factories.MessageFactory(thread=thread3)

        # Authenticate user
        client = APIClient()
        client.force_authenticate(user=authenticated_user)

        # Get list of mailboxes
        response = client.get(reverse("mailboxes-list"))
        assert response.status_code == status.HTTP_200_OK
        assert len(response.data) == 2

        # Check response data
        assert response.data[0]["id"] == str(user_mailbox2.id)
        assert response.data[0]["email"] == str(user_mailbox2)
        assert response.data[0]["role"] == "editor"
        assert response.data[0]["is_identity"] is True
        assert response.data[0]["count_unread_threads"] == 1
        assert response.data[0]["count_threads"] == 2
        assert response.data[0]["count_delivering"] == 1
        assert response.data[0]["count_unread_mentions"] == 0
        assert response.data[0]["count_assigned"] == 0

        assert response.data[1]["id"] == str(user_mailbox1.id)
        assert response.data[1]["email"] == str(user_mailbox1)
        assert response.data[1]["role"] == "viewer"
        assert response.data[1]["is_identity"] is True
        assert response.data[1]["count_unread_threads"] == 1
        assert response.data[1]["count_threads"] == 1
        assert response.data[1]["count_delivering"] == 1
        assert response.data[1]["count_unread_mentions"] == 0
        assert response.data[1]["count_assigned"] == 0

    def test_list_is_identity_false(self):
        """A mailbox that is not an identity should return is_identity=False."""
        user = factories.UserFactory()
        mailbox = factories.MailboxFactory(is_identity=False)
        factories.MailboxAccessFactory(
            mailbox=mailbox,
            user=user,
            role=models.MailboxRoleChoices.VIEWER,
        )

        client = APIClient()
        client.force_authenticate(user=user)
        response = client.get(reverse("mailboxes-list"))
        assert response.status_code == status.HTTP_200_OK
        assert response.data[0]["is_identity"] is False

    def test_list_is_shared_solo_identity_is_false(self):
        """An identity mailbox with a single access is not shared."""
        user = factories.UserFactory()
        mailbox = factories.MailboxFactory(is_identity=True)
        factories.MailboxAccessFactory(
            mailbox=mailbox,
            user=user,
            role=models.MailboxRoleChoices.EDITOR,
        )

        client = APIClient()
        client.force_authenticate(user=user)
        response = client.get(reverse("mailboxes-list"))
        assert response.status_code == status.HTTP_200_OK
        assert response.data[0]["is_shared"] is False

    def test_list_is_shared_identity_with_multiple_accesses_is_true(self):
        """An identity mailbox shared via delegation (>1 access) is shared."""
        user = factories.UserFactory()
        delegate = factories.UserFactory()
        mailbox = factories.MailboxFactory(is_identity=True)
        factories.MailboxAccessFactory(
            mailbox=mailbox,
            user=user,
            role=models.MailboxRoleChoices.EDITOR,
        )
        factories.MailboxAccessFactory(
            mailbox=mailbox,
            user=delegate,
            role=models.MailboxRoleChoices.VIEWER,
        )

        client = APIClient()
        client.force_authenticate(user=user)
        response = client.get(reverse("mailboxes-list"))
        assert response.status_code == status.HTTP_200_OK
        assert response.data[0]["is_shared"] is True

    def test_list_is_shared_non_identity_is_true(self):
        """A non-identity mailbox is always shared, even with a single access."""
        user = factories.UserFactory()
        mailbox = factories.MailboxFactory(is_identity=False)
        factories.MailboxAccessFactory(
            mailbox=mailbox,
            user=user,
            role=models.MailboxRoleChoices.EDITOR,
        )

        client = APIClient()
        client.force_authenticate(user=user)
        response = client.get(reverse("mailboxes-list"))
        assert response.status_code == status.HTTP_200_OK
        assert response.data[0]["is_shared"] is True

    def test_list_unauthorized(self):
        """Anonymous user cannot access the list of mailboxes."""
        client = APIClient()
        response = client.get(reverse("mailboxes-list"))
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_search_mailboxes(self):
        """Test searching mailboxes by domain and query."""
        # Create authenticated user
        authenticated_user = factories.UserFactory()
        client = APIClient()
        client.force_authenticate(user=authenticated_user)

        # Create mailboxes in the same domain
        domain = factories.MailDomainFactory(name="mydomain.com")
        context_contact = factories.ContactFactory(name="Context User")
        context_mailbox = factories.MailboxFactory(
            domain=domain, contact=context_contact, local_part="context"
        )

        # Create mailboxes with contacts
        john_doe_contact = factories.ContactFactory(name="John Doe")
        john_doe_mailbox = factories.MailboxFactory(
            domain=domain, contact=john_doe_contact, local_part="john.doe"
        )
        jane_doe_contact = factories.ContactFactory(name="Jane Doe")
        jane_doe_mailbox = factories.MailboxFactory(
            domain=domain, contact=jane_doe_contact, local_part="jane.doe"
        )
        john_smith_contact = factories.ContactFactory(name="John Smith")
        john_smith_mailbox = factories.MailboxFactory(
            domain=domain, contact=john_smith_contact, local_part="john.smith"
        )
        other_contact = factories.ContactFactory(name="Other User")
        factories.MailboxFactory(contact=other_contact, local_part="other")

        # Give user access to source mailbox
        factories.MailboxAccessFactory(
            mailbox=context_mailbox,
            user=authenticated_user,
            role=models.MailboxRoleChoices.EDITOR,
        )

        # Test search by domain only (no query)
        response = client.get(
            reverse("mailboxes-search", kwargs={"pk": str(context_mailbox.id)}),
        )
        assert response.status_code == status.HTTP_200_OK
        assert (
            len(response.data) == 3
        )  # All mailboxes in example.com domain except context mailbox
        assert {mailbox["id"] for mailbox in response.data} == {
            str(john_doe_mailbox.id),
            str(jane_doe_mailbox.id),
            str(john_smith_mailbox.id),
        }
        # TODO:exclude current mailbox ?

        # Test search by local part
        response = client.get(
            reverse("mailboxes-search", kwargs={"pk": str(context_mailbox.id)}),
            {"q": "john"},
        )
        assert response.status_code == status.HTTP_200_OK
        assert len(response.data) == 2  # john.doe and john.smith
        assert {mailbox["id"] for mailbox in response.data} == {
            str(john_doe_mailbox.id),
            str(john_smith_mailbox.id),
        }

        # Test search by contact name
        response = client.get(
            reverse("mailboxes-search", kwargs={"pk": str(context_mailbox.id)}),
            {"q": "doe"},
        )
        assert response.status_code == status.HTTP_200_OK

        assert len(response.data) == 2  # john.doe and jane.doe
        assert {mailbox["id"] for mailbox in response.data} == {
            str(john_doe_mailbox.id),
            str(jane_doe_mailbox.id),
        }

        # Test search by both local part and contact name
        response = client.get(
            reverse("mailboxes-search", kwargs={"pk": str(context_mailbox.id)}),
            {"q": "jane doe"},
        )
        assert response.status_code == status.HTTP_200_OK
        assert len(response.data) == 1  # only jane.doe
        assert response.data[0]["id"] == str(jane_doe_mailbox.id)

        # Test search with no matches
        response = client.get(
            reverse("mailboxes-search", kwargs={"pk": str(context_mailbox.id)}),
            {"q": "nonexistent"},
        )
        assert response.status_code == status.HTTP_200_OK
        assert len(response.data) == 0

    def test_search_mailboxes_errors(self):
        """Test error cases for mailbox search."""
        authenticated_user = factories.UserFactory()
        client = APIClient()
        client.force_authenticate(user=authenticated_user)

        # Test invalid UUID format
        response = client.get(
            reverse("mailboxes-search", kwargs={"pk": "invalid-uuid"}),
        )
        assert response.status_code == status.HTTP_404_NOT_FOUND

        # Test non-existent mailbox
        response = client.get(
            reverse(
                "mailboxes-search",
                kwargs={"pk": "00000000-0000-0000-0000-000000000000"},
            ),
        )
        assert response.status_code == status.HTTP_404_NOT_FOUND

    def test_search_mailboxes_unauthorized(self):
        """Test that anonymous users cannot search mailboxes."""
        client = APIClient()
        response = client.get(
            reverse(
                "mailboxes-search",
                kwargs={"pk": "00000000-0000-0000-0000-000000000000"},
            ),
        )
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_search_mailboxes_without_access(self):
        """Test that users cannot search mailboxes they don't have access to."""
        # Create two users
        user1 = factories.UserFactory()
        user2 = factories.UserFactory()

        # Create a mailbox for user1
        domain = factories.MailDomainFactory(name="example.com")
        mailbox = factories.MailboxFactory(local_part="user1", domain=domain)
        factories.MailboxAccessFactory(
            mailbox=mailbox,
            user=user1,
            role=models.MailboxRoleChoices.EDITOR,
        )

        # Try to search using user2's credentials
        client = APIClient()
        client.force_authenticate(user=user2)

        response = client.get(
            reverse("mailboxes-search", kwargs={"pk": str(mailbox.id)}),
        )
        assert response.status_code == status.HTTP_404_NOT_FOUND

    def test_retrieve_mailbox(self):
        """Test the retrieve method with user_access_roles optimization."""
        # Create authenticated user with access to a mailbox
        authenticated_user = factories.UserFactory()
        mailbox = factories.MailboxFactory()
        factories.MailboxAccessFactory(
            mailbox=mailbox,
            user=authenticated_user,
            role=models.MailboxRoleChoices.EDITOR,
        )

        # Create a thread with one unread message for the mailbox (with delivering)
        thread = factories.ThreadFactory(
            has_delivery_pending=True,
            messaged_at=timezone.now(),
            has_active=True,
            active_messaged_at=timezone.now(),
        )
        factories.ThreadAccessFactory(
            mailbox=mailbox,
            thread=thread,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        factories.MessageFactory(thread=thread)

        # Authenticate user
        client = APIClient()
        client.force_authenticate(user=authenticated_user)

        # Get mailbox detail
        response = client.get(
            reverse("mailboxes-detail", kwargs={"pk": str(mailbox.id)})
        )
        assert response.status_code == status.HTTP_200_OK

        # Check response data
        assert response.data["id"] == str(mailbox.id)
        assert response.data["email"] == str(mailbox)
        assert response.data["domain_id"] == str(mailbox.domain_id)
        assert response.data["role"] == "editor"
        assert response.data["count_unread_threads"] == 1
        assert response.data["count_threads"] == 1
        assert response.data["count_delivering"] == 1
        assert response.data["count_unread_mentions"] == 0
        assert response.data["count_assigned"] == 0

    def test_list_count_unread_mentions(self):
        """count_unread_mentions should count distinct threads with an active
        unread MENTION UserEvent for the current user, scoped per mailbox."""
        user = factories.UserFactory()
        other_user = factories.UserFactory()

        mailbox_a = factories.MailboxFactory()
        mailbox_b = factories.MailboxFactory()
        factories.MailboxAccessFactory(
            mailbox=mailbox_a, user=user, role=models.MailboxRoleChoices.EDITOR
        )
        factories.MailboxAccessFactory(
            mailbox=mailbox_b, user=user, role=models.MailboxRoleChoices.EDITOR
        )

        # Thread in mailbox_a with one unread mention for `user`
        thread_a1 = factories.ThreadFactory()
        factories.ThreadAccessFactory(
            mailbox=mailbox_a,
            thread=thread_a1,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        event_a1 = factories.ThreadEventFactory(thread=thread_a1, author=other_user)
        factories.UserEventFactory(
            user=user,
            thread=thread_a1,
            thread_event=event_a1,
            type=enums.UserEventTypeChoices.MENTION,
        )

        # Thread in mailbox_a with two unread mentions for `user` — still counted once
        thread_a2 = factories.ThreadFactory()
        factories.ThreadAccessFactory(
            mailbox=mailbox_a,
            thread=thread_a2,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        event_a2a = factories.ThreadEventFactory(thread=thread_a2, author=other_user)
        event_a2b = factories.ThreadEventFactory(thread=thread_a2, author=other_user)
        factories.UserEventFactory(
            user=user,
            thread=thread_a2,
            thread_event=event_a2a,
            type=enums.UserEventTypeChoices.MENTION,
        )
        factories.UserEventFactory(
            user=user,
            thread=thread_a2,
            thread_event=event_a2b,
            type=enums.UserEventTypeChoices.MENTION,
        )

        # Thread in mailbox_a with a mention already read — must be excluded
        thread_a3 = factories.ThreadFactory()
        factories.ThreadAccessFactory(
            mailbox=mailbox_a,
            thread=thread_a3,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        event_a3 = factories.ThreadEventFactory(thread=thread_a3, author=other_user)
        factories.UserEventFactory(
            user=user,
            thread=thread_a3,
            thread_event=event_a3,
            type=enums.UserEventTypeChoices.MENTION,
            read_at=timezone.now(),
        )

        # Thread in mailbox_a mentioning someone else — must be excluded
        thread_a5 = factories.ThreadFactory()
        factories.ThreadAccessFactory(
            mailbox=mailbox_a,
            thread=thread_a5,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        event_a5 = factories.ThreadEventFactory(thread=thread_a5, author=user)
        factories.UserEventFactory(
            user=other_user,
            thread=thread_a5,
            thread_event=event_a5,
            type=enums.UserEventTypeChoices.MENTION,
        )

        # Thread in mailbox_b with one unread mention — scoping check
        thread_b1 = factories.ThreadFactory()
        factories.ThreadAccessFactory(
            mailbox=mailbox_b,
            thread=thread_b1,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        event_b1 = factories.ThreadEventFactory(thread=thread_b1, author=other_user)
        factories.UserEventFactory(
            user=user,
            thread=thread_b1,
            thread_event=event_b1,
            type=enums.UserEventTypeChoices.MENTION,
        )

        client = APIClient()
        client.force_authenticate(user=user)
        response = client.get(reverse("mailboxes-list"))
        assert response.status_code == status.HTTP_200_OK

        by_id = {m["id"]: m for m in response.data}
        assert by_id[str(mailbox_a.id)]["count_unread_mentions"] == 2
        assert by_id[str(mailbox_b.id)]["count_unread_mentions"] == 1

    def test_list_count_assigned(self):
        """count_assigned should count distinct threads in this mailbox where
        the current user is currently assigned. UserEvent(type=ASSIGN) is the
        source of truth (created on assign, deleted on unassign), so the
        counter reflects the live state without read-state filtering."""
        user = factories.UserFactory()
        other_user = factories.UserFactory()

        mailbox_a = factories.MailboxFactory()
        mailbox_b = factories.MailboxFactory()
        factories.MailboxAccessFactory(
            mailbox=mailbox_a, user=user, role=models.MailboxRoleChoices.EDITOR
        )
        factories.MailboxAccessFactory(
            mailbox=mailbox_b, user=user, role=models.MailboxRoleChoices.EDITOR
        )

        # Thread in mailbox_a where `user` is assigned
        thread_a1 = factories.ThreadFactory()
        factories.ThreadAccessFactory(
            mailbox=mailbox_a,
            thread=thread_a1,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        event_a1 = factories.ThreadEventFactory(thread=thread_a1, author=other_user)
        factories.UserEventFactory(
            user=user,
            thread=thread_a1,
            thread_event=event_a1,
            type=enums.UserEventTypeChoices.ASSIGN,
        )

        # Thread in mailbox_a assigned to someone else — must be excluded
        thread_a3 = factories.ThreadFactory()
        factories.ThreadAccessFactory(
            mailbox=mailbox_a,
            thread=thread_a3,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        event_a3 = factories.ThreadEventFactory(thread=thread_a3, author=user)
        factories.UserEventFactory(
            user=other_user,
            thread=thread_a3,
            thread_event=event_a3,
            type=enums.UserEventTypeChoices.ASSIGN,
        )

        # Thread in mailbox_a where `user` only has a MENTION (not ASSIGN) —
        # must be excluded; cross-checks that the counter is type-scoped.
        thread_a4 = factories.ThreadFactory()
        factories.ThreadAccessFactory(
            mailbox=mailbox_a,
            thread=thread_a4,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        event_a4 = factories.ThreadEventFactory(thread=thread_a4, author=other_user)
        factories.UserEventFactory(
            user=user,
            thread=thread_a4,
            thread_event=event_a4,
            type=enums.UserEventTypeChoices.MENTION,
        )

        # Thread in mailbox_b assigned to `user` — scoping check
        thread_b1 = factories.ThreadFactory()
        factories.ThreadAccessFactory(
            mailbox=mailbox_b,
            thread=thread_b1,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        event_b1 = factories.ThreadEventFactory(thread=thread_b1, author=other_user)
        factories.UserEventFactory(
            user=user,
            thread=thread_b1,
            thread_event=event_b1,
            type=enums.UserEventTypeChoices.ASSIGN,
        )

        client = APIClient()
        client.force_authenticate(user=user)
        response = client.get(reverse("mailboxes-list"))
        assert response.status_code == status.HTTP_200_OK

        by_id = {m["id"]: m for m in response.data}
        assert by_id[str(mailbox_a.id)]["count_assigned"] == 1
        assert by_id[str(mailbox_b.id)]["count_assigned"] == 1

    def test_retrieve_mailbox_unauthorized(self):
        """Test that users cannot retrieve mailboxes they don't have access to."""
        # Create user without access to mailbox
        user = factories.UserFactory()
        mailbox = factories.MailboxFactory()

        # Authenticate user
        client = APIClient()
        client.force_authenticate(user=user)

        # Try to get mailbox detail
        response = client.get(
            reverse("mailboxes-detail", kwargs={"pk": str(mailbox.id)})
        )
        assert response.status_code == status.HTTP_404_NOT_FOUND

    def test_search_mailboxes_case_insensitive(self):
        """Test that search is case insensitive."""
        authenticated_user = factories.UserFactory()
        client = APIClient()
        client.force_authenticate(user=authenticated_user)

        # Create mailboxes with mixed case
        domain = factories.MailDomainFactory(name="example.com")
        context_mailbox = factories.MailboxFactory(domain=domain)
        pierre_bidule_contact = factories.ContactFactory(name="Pierre Bidule")
        factories.MailboxFactory(
            local_part="pierre.bidule", domain=domain, contact=pierre_bidule_contact
        )
        jane_bidule_contact = factories.ContactFactory(name="JANE BIDULE")
        factories.MailboxFactory(
            local_part="jane.bidule", domain=domain, contact=jane_bidule_contact
        )

        # Give user access to source mailbox
        factories.MailboxAccessFactory(
            mailbox=context_mailbox,
            user=authenticated_user,
            role=models.MailboxRoleChoices.EDITOR,
        )

        # Test case insensitive search for local part
        response = client.get(
            reverse("mailboxes-search", kwargs={"pk": str(context_mailbox.id)}),
            {"q": "pierre"},
        )
        assert response.status_code == status.HTTP_200_OK
        assert len(response.data) == 1

        # Test case insensitive search for contact name
        response = client.get(
            reverse("mailboxes-search", kwargs={"pk": str(context_mailbox.id)}),
            {"q": "jane bidule"},
        )
        assert response.status_code == status.HTTP_200_OK
        assert len(response.data) == 1


class TestMailboxAbilitiesAPI:
    """Test the abilities field in Mailbox API responses."""

    @override_settings(FEATURE_MESSAGE_TEMPLATES=True, FEATURE_IMPORT_MESSAGES=True)
    def test_mailbox_abilities_in_response(self, api_client, user, mailbox):
        """Test that abilities are included in mailbox API response."""
        models.MailboxAccess.objects.create(
            mailbox=mailbox,
            user=user,
            role=models.MailboxRoleChoices.ADMIN,
        )

        api_client.force_authenticate(user=user)
        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.get(url)

        assert response.status_code == status.HTTP_200_OK
        assert "abilities" in response.data
        abilities = response.data["abilities"]
        assert abilities["get"] is True
        assert abilities["patch"] is True
        assert abilities["put"] is True
        assert abilities["post"] is True
        assert abilities["delete"] is True
        assert abilities["manage_accesses"] is True
        assert abilities["view_messages"] is True
        assert abilities["send_messages"] is True
        assert abilities["manage_labels"] is True
        assert abilities["manage_message_templates"] is True
        assert abilities["import_messages"] is True

    def test_mailbox_list_with_abilities(self, api_client, user, mailbox):
        """Test that mailbox list includes abilities for each mailbox."""
        models.MailboxAccess.objects.create(
            mailbox=mailbox,
            user=user,
            role=models.MailboxRoleChoices.EDITOR,
        )

        api_client.force_authenticate(user=user)
        url = reverse("mailboxes-list")
        response = api_client.get(url)

        assert response.status_code == status.HTTP_200_OK
        assert len(response.data) == 1

        mailbox_data = response.data[0]
        assert "abilities" in mailbox_data
        abilities = mailbox_data["abilities"]
        assert abilities["get"] is True
        assert abilities["patch"] is True
        assert abilities["put"] is True
        assert abilities["post"] is True
        assert abilities["delete"] is False
        assert abilities["manage_accesses"] is False
        assert abilities["view_messages"] is True
        assert abilities["send_messages"] is False
        assert abilities["manage_labels"] is True
        assert abilities["manage_message_templates"] is False
        assert abilities["import_messages"] is False

    def test_mailbox_detail_no_access_abilities(self, api_client, user, mailbox):
        """Test that abilities are correctly set when user has no access to detail."""
        api_client.force_authenticate(user=user)
        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.get(url)

        # Should return 404 since user has no access to this mailbox
        assert response.status_code == status.HTTP_404_NOT_FOUND

    # pylint: disable=unused-argument
    def test_mailbox_list_no_access_abilities(self, api_client, user, mailbox):
        """Test that abilities are correctly set when user has no access."""
        api_client.force_authenticate(user=user)
        url = reverse("mailboxes-list")
        response = api_client.get(url)

        # User has no access, so should get empty list
        assert response.status_code == status.HTTP_200_OK
        assert len(response.data) == 0

    def test_mailbox_viewer_abilities(self, api_client, user, mailbox):
        """Test that viewer role has correct abilities."""
        models.MailboxAccess.objects.create(
            mailbox=mailbox,
            user=user,
            role=models.MailboxRoleChoices.VIEWER,
        )

        api_client.force_authenticate(user=user)
        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.get(url)

        assert response.status_code == status.HTTP_200_OK
        assert "abilities" in response.data
        abilities = response.data["abilities"]
        assert abilities["get"] is True
        assert abilities["patch"] is False
        assert abilities["put"] is False
        assert abilities["post"] is False
        assert abilities["delete"] is False
        assert abilities["manage_accesses"] is False
        assert abilities["view_messages"] is True
        assert abilities["send_messages"] is False
        assert abilities["manage_labels"] is False
        assert abilities["manage_message_templates"] is False
        assert abilities["import_messages"] is False

    def test_mailbox_user_role_annotation(self, api_client, user, mailbox):
        """Test that the user_role annotation works correctly."""
        # Create access for user
        models.MailboxAccess.objects.create(
            mailbox=mailbox,
            user=user,
            role=models.MailboxRoleChoices.EDITOR,
        )

        api_client.force_authenticate(user=user)

        # Test list endpoint
        url = reverse("mailboxes-list")
        response = api_client.get(url)
        assert response.status_code == status.HTTP_200_OK
        assert len(response.data) == 1
        assert response.data[0]["role"] == "editor"

        # Test detail endpoint
        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.get(url)
        assert response.status_code == status.HTTP_200_OK
        assert response.data["role"] == "editor"


@pytest.mark.django_db
class TestMailboxPartialUpdate:
    """Test renaming a mailbox via PATCH /mailboxes/{id}/ (mailbox admins)."""

    def _grant(self, mailbox, user, role):
        return factories.MailboxAccessFactory(mailbox=mailbox, user=user, role=role)

    def test_admin_can_rename_and_creates_missing_contact(
        self, api_client, user, mailbox
    ):
        """A mailbox admin can rename a mailbox; a missing contact is created."""
        self._grant(mailbox, user, models.MailboxRoleChoices.ADMIN)
        assert mailbox.contact_id is None
        api_client.force_authenticate(user=user)

        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.patch(url, data={"name": "Helpdesk"}, format="json")

        assert response.status_code == status.HTTP_200_OK
        assert response.data["name"] == "Helpdesk"
        mailbox.refresh_from_db()
        assert mailbox.contact is not None
        assert mailbox.contact.name == "Helpdesk"
        assert mailbox.contact.email == str(mailbox)

    def test_admin_can_rename_existing_contact(self, api_client, user):
        """Renaming a mailbox that already has a contact updates its name."""
        contact = factories.ContactFactory(name="Old")
        mailbox = factories.MailboxFactory(contact=contact)
        contact.mailbox = mailbox
        contact.email = str(mailbox)
        contact.save()
        self._grant(mailbox, user, models.MailboxRoleChoices.ADMIN)
        api_client.force_authenticate(user=user)

        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.patch(url, data={"name": "New"}, format="json")

        assert response.status_code == status.HTTP_200_OK
        mailbox.refresh_from_db()
        assert mailbox.contact.name == "New"

    @pytest.mark.parametrize(
        "role",
        [models.MailboxRoleChoices.VIEWER, models.MailboxRoleChoices.EDITOR],
    )
    def test_non_admin_member_forbidden(self, api_client, user, mailbox, role):
        """A non-admin member of the mailbox cannot rename it."""
        self._grant(mailbox, user, role)
        api_client.force_authenticate(user=user)

        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.patch(url, data={"name": "Nope"}, format="json")

        assert response.status_code == status.HTTP_403_FORBIDDEN

    def test_non_member_not_found(self, api_client, user, mailbox):
        """A user with no access to the mailbox gets a 404."""
        api_client.force_authenticate(user=user)
        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.patch(url, data={"name": "Nope"}, format="json")
        assert response.status_code == status.HTTP_404_NOT_FOUND

    def test_unauthenticated(self, api_client, mailbox):
        """An unauthenticated request is rejected."""
        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.patch(url, data={"name": "Nope"}, format="json")
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    def test_blank_name_rejected(self, api_client, user, mailbox):
        """An empty name is rejected with a 400."""
        self._grant(mailbox, user, models.MailboxRoleChoices.ADMIN)
        api_client.force_authenticate(user=user)
        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.patch(url, data={"name": ""}, format="json")
        assert response.status_code == status.HTTP_400_BAD_REQUEST

    def test_whitespace_only_name_rejected(self, api_client, user, mailbox):
        """A whitespace-only name is rejected with a 400 (no blank rename)."""
        self._grant(mailbox, user, models.MailboxRoleChoices.ADMIN)
        api_client.force_authenticate(user=user)
        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.patch(url, data={"name": "   "}, format="json")
        assert response.status_code == status.HTTP_400_BAD_REQUEST

    def test_only_name_is_updated(self, api_client, user):
        """Extra fields in the payload are ignored: only ``name`` is applied."""
        mailbox = factories.MailboxFactory(local_part="support", is_identity=False)
        self._grant(mailbox, user, models.MailboxRoleChoices.ADMIN)
        api_client.force_authenticate(user=user)

        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.patch(
            url,
            data={"name": "Renamed", "local_part": "hijacked", "is_identity": True},
            format="json",
        )

        assert response.status_code == status.HTTP_200_OK
        assert response.data["name"] == "Renamed"
        mailbox.refresh_from_db()
        assert mailbox.contact.name == "Renamed"
        # Fields outside the dedicated serializer stay untouched.
        assert mailbox.local_part == "support"
        assert mailbox.is_identity is False

    def test_missing_name_is_noop(self, api_client, user):
        """An empty body leaves the name untouched (partial PATCH no-op)."""
        contact = factories.ContactFactory(name="Old")
        mailbox = factories.MailboxFactory(contact=contact)
        contact.mailbox = mailbox
        contact.email = str(mailbox)
        contact.save()
        self._grant(mailbox, user, models.MailboxRoleChoices.ADMIN)
        api_client.force_authenticate(user=user)

        url = reverse("mailboxes-detail", args=[mailbox.id])
        response = api_client.patch(url, data={}, format="json")

        assert response.status_code == status.HTTP_200_OK
        assert response.data["name"] == "Old"
        mailbox.refresh_from_db()
        assert mailbox.contact.name == "Old"
