# tests/unit/services/test_messaging_service.py
"""Unit tests for MessagingService."""

import pytest

from app.modules.messaging.models import (
    ConversationParticipant,
    ConversationType,
    ParticipantType,
)
from app.modules.messaging.services.messaging_service import MessagingService


@pytest.fixture
def messaging_service():
    """Create a MessagingService instance."""
    return MessagingService()


@pytest.mark.unit
class TestMessagingServiceCreateConversation:
    """Test conversation creation."""

    def test_create_conversation_admin_store(
        self, db, messaging_service, test_admin, test_store_user, test_store
    ):
        """Test creating an admin-store conversation."""
        conversation = messaging_service.create_conversation(
            db=db,
            conversation_type=ConversationType.ADMIN_STORE,
            subject="Test Subject",
            initiator_type=ParticipantType.ADMIN,
            initiator_id=test_admin.id,
            recipient_type=ParticipantType.STORE,
            recipient_id=test_store_user.id,
            store_id=test_store.id,
        )
        db.commit()

        assert conversation.id is not None
        assert conversation.conversation_type == ConversationType.ADMIN_STORE
        assert conversation.subject == "Test Subject"
        assert conversation.store_id == test_store.id
        assert conversation.is_closed is False
        assert len(conversation.participants) == 2

    def test_create_conversation_store_customer(
        self, db, messaging_service, test_store_user, test_customer, test_store
    ):
        """Test creating a store-customer conversation."""
        conversation = messaging_service.create_conversation(
            db=db,
            conversation_type=ConversationType.STORE_CUSTOMER,
            subject="Customer Support",
            initiator_type=ParticipantType.STORE,
            initiator_id=test_store_user.id,
            recipient_type=ParticipantType.CUSTOMER,
            recipient_id=test_customer.id,
            store_id=test_store.id,
        )
        db.commit()

        assert conversation.id is not None
        assert conversation.conversation_type == ConversationType.STORE_CUSTOMER
        assert len(conversation.participants) == 2

        # Verify participants
        participant_types = [p.participant_type for p in conversation.participants]
        assert ParticipantType.STORE in participant_types
        assert ParticipantType.CUSTOMER in participant_types

    def test_create_conversation_admin_customer(
        self, db, messaging_service, test_admin, test_customer, test_store
    ):
        """Test creating an admin-customer conversation."""
        conversation = messaging_service.create_conversation(
            db=db,
            conversation_type=ConversationType.ADMIN_CUSTOMER,
            subject="Platform Support",
            initiator_type=ParticipantType.ADMIN,
            initiator_id=test_admin.id,
            recipient_type=ParticipantType.CUSTOMER,
            recipient_id=test_customer.id,
            store_id=test_store.id,
        )
        db.commit()

        assert conversation.conversation_type == ConversationType.ADMIN_CUSTOMER
        assert len(conversation.participants) == 2

    def test_create_conversation_with_initial_message(
        self, db, messaging_service, test_admin, test_store_user, test_store
    ):
        """Test creating a conversation with an initial message."""
        conversation = messaging_service.create_conversation(
            db=db,
            conversation_type=ConversationType.ADMIN_STORE,
            subject="With Message",
            initiator_type=ParticipantType.ADMIN,
            initiator_id=test_admin.id,
            recipient_type=ParticipantType.STORE,
            recipient_id=test_store_user.id,
            store_id=test_store.id,
            initial_message="Hello, this is the first message!",
        )
        db.commit()
        db.refresh(conversation)

        assert conversation.message_count == 1
        assert len(conversation.messages) == 1
        assert conversation.messages[0].content == "Hello, this is the first message!"

    def test_create_store_customer_without_store_id_fails(
        self, db, messaging_service, test_store_user, test_customer
    ):
        """Test that store_customer conversation requires store_id."""
        with pytest.raises(ValueError) as exc_info:
            messaging_service.create_conversation(
                db=db,
                conversation_type=ConversationType.STORE_CUSTOMER,
                subject="No Store",
                initiator_type=ParticipantType.STORE,
                initiator_id=test_store_user.id,
                recipient_type=ParticipantType.CUSTOMER,
                recipient_id=test_customer.id,
                store_id=None,
            )
        assert "store_id required" in str(exc_info.value)


@pytest.mark.unit
class TestMessagingServiceGetConversation:
    """Test conversation retrieval."""

    def test_get_conversation_success(
        self, db, messaging_service, test_conversation_admin_store, test_admin
    ):
        """Test getting a conversation by ID."""
        conversation = messaging_service.get_conversation(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
        )

        assert conversation is not None
        assert conversation.id == test_conversation_admin_store.id
        assert conversation.subject == "Test Admin-Store Conversation"

    def test_get_conversation_not_found(self, db, messaging_service, test_admin):
        """Test getting a non-existent conversation."""
        conversation = messaging_service.get_conversation(
            db=db,
            conversation_id=99999,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
        )

        assert conversation is None

    def test_get_conversation_unauthorized(
        self, db, messaging_service, test_conversation_admin_store, test_customer
    ):
        """Test getting a conversation without access."""
        # Customer is not a participant in admin-store conversation
        conversation = messaging_service.get_conversation(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            participant_type=ParticipantType.CUSTOMER,
            participant_id=test_customer.id,
        )

        assert conversation is None


@pytest.mark.unit
class TestMessagingServiceListConversations:
    """Test conversation listing."""

    def test_list_conversations_success(
        self, db, messaging_service, multiple_conversations, test_admin
    ):
        """Test listing conversations for a participant."""
        conversations, total, total_unread = messaging_service.list_conversations(
            db=db,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
        )

        # Admin should see all admin-store conversations (3 of them)
        assert total == 3
        assert len(conversations) == 3

    def test_list_conversations_with_type_filter(
        self, db, messaging_service, multiple_conversations, test_store_user, test_store
    ):
        """Test filtering conversations by type."""
        # Store should see admin-store (3) + store-customer (2) = 5
        # Filter to store-customer only
        conversations, total, _ = messaging_service.list_conversations(
            db=db,
            participant_type=ParticipantType.STORE,
            participant_id=test_store_user.id,
            store_id=test_store.id,
            conversation_type=ConversationType.STORE_CUSTOMER,
        )

        assert total == 2
        for conv in conversations:
            assert conv.conversation_type == ConversationType.STORE_CUSTOMER

    def test_list_conversations_pagination(
        self, db, messaging_service, multiple_conversations, test_admin
    ):
        """Test pagination of conversations."""
        # First page
        conversations, total, _ = messaging_service.list_conversations(
            db=db,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
            skip=0,
            limit=2,
        )

        assert total == 3
        assert len(conversations) == 2

        # Second page
        conversations, total, _ = messaging_service.list_conversations(
            db=db,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
            skip=2,
            limit=2,
        )

        assert total == 3
        assert len(conversations) == 1

    def test_list_conversations_with_closed_filter(
        self, db, messaging_service, test_conversation_admin_store, closed_conversation, test_admin
    ):
        """Test filtering by open/closed status."""
        # Only open
        conversations, total, _ = messaging_service.list_conversations(
            db=db,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
            is_closed=False,
        )
        assert total == 1
        assert all(not conv.is_closed for conv in conversations)

        # Only closed
        conversations, total, _ = messaging_service.list_conversations(
            db=db,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
            is_closed=True,
        )
        assert total == 1
        assert all(conv.is_closed for conv in conversations)


@pytest.mark.unit
class TestMessagingServiceSendMessage:
    """Test message sending."""

    def test_send_message_success(
        self, db, messaging_service, test_conversation_admin_store, test_admin
    ):
        """Test sending a message."""
        message = messaging_service.send_message(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            sender_type=ParticipantType.ADMIN,
            sender_id=test_admin.id,
            content="Hello, this is a test message!",
        )
        db.commit()

        assert message.id is not None
        assert message.content == "Hello, this is a test message!"
        assert message.sender_type == ParticipantType.ADMIN
        assert message.sender_id == test_admin.id
        assert message.conversation_id == test_conversation_admin_store.id

        # Verify conversation was updated
        db.refresh(test_conversation_admin_store)
        assert test_conversation_admin_store.message_count == 1
        assert test_conversation_admin_store.last_message_at is not None

    def test_send_message_with_attachments(
        self, db, messaging_service, test_conversation_admin_store, test_admin
    ):
        """Test sending a message with attachments."""
        attachments = [
            {
                "filename": "doc1.pdf",
                "original_filename": "document.pdf",
                "file_path": "/uploads/messages/2025/01/1/doc1.pdf",
                "file_size": 12345,
                "mime_type": "application/pdf",
                "is_image": False,
            }
        ]

        message = messaging_service.send_message(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            sender_type=ParticipantType.ADMIN,
            sender_id=test_admin.id,
            content="See attached document.",
            attachments=attachments,
        )
        db.commit()
        db.refresh(message)

        assert len(message.attachments) == 1
        assert message.attachments[0].original_filename == "document.pdf"

    def test_send_message_updates_unread_count(
        self, db, messaging_service, test_conversation_admin_store, test_admin, test_store_user
    ):
        """Test that sending a message updates unread count for other participants."""
        # Send message as admin
        messaging_service.send_message(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            sender_type=ParticipantType.ADMIN,
            sender_id=test_admin.id,
            content="Test message",
        )
        db.commit()

        # Check that store user has unread count increased
        store_participant = (
            db.query(ConversationParticipant)
            .filter(
                ConversationParticipant.conversation_id == test_conversation_admin_store.id,
                ConversationParticipant.participant_type == ParticipantType.STORE,
                ConversationParticipant.participant_id == test_store_user.id,
            )
            .first()
        )
        assert store_participant.unread_count == 1

        # Admin's unread count should be 0
        admin_participant = (
            db.query(ConversationParticipant)
            .filter(
                ConversationParticipant.conversation_id == test_conversation_admin_store.id,
                ConversationParticipant.participant_type == ParticipantType.ADMIN,
                ConversationParticipant.participant_id == test_admin.id,
            )
            .first()
        )
        assert admin_participant.unread_count == 0

    def test_send_system_message(
        self, db, messaging_service, test_conversation_admin_store, test_admin
    ):
        """Test sending a system message."""
        message = messaging_service.send_message(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            sender_type=ParticipantType.ADMIN,
            sender_id=test_admin.id,
            content="Conversation closed",
            is_system_message=True,
        )
        db.commit()

        assert message.is_system_message is True


@pytest.mark.unit
class TestMessagingServiceMarkRead:
    """Test marking conversations as read."""

    def test_mark_conversation_read(
        self, db, messaging_service, test_conversation_admin_store, test_admin, test_store_user
    ):
        """Test marking a conversation as read."""
        # Send a message to create unread count
        messaging_service.send_message(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            sender_type=ParticipantType.ADMIN,
            sender_id=test_admin.id,
            content="Test message",
        )
        db.commit()

        # Mark as read for store
        result = messaging_service.mark_conversation_read(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            reader_type=ParticipantType.STORE,
            reader_id=test_store_user.id,
        )
        db.commit()

        assert result is True

        # Verify unread count is reset
        store_participant = (
            db.query(ConversationParticipant)
            .filter(
                ConversationParticipant.conversation_id == test_conversation_admin_store.id,
                ConversationParticipant.participant_type == ParticipantType.STORE,
            )
            .first()
        )
        assert store_participant.unread_count == 0
        assert store_participant.last_read_at is not None


@pytest.mark.unit
class TestMessagingServiceUnreadCount:
    """Test unread count retrieval."""

    def test_get_unread_count(
        self, db, messaging_service, multiple_conversations, test_admin, test_store_user
    ):
        """Test getting total unread count for a participant."""
        # Send messages in multiple conversations (first 2 are admin-store)
        for conv in multiple_conversations[:2]:
            messaging_service.send_message(
                db=db,
                conversation_id=conv.id,
                sender_type=ParticipantType.STORE,
                sender_id=test_store_user.id,
                content="Test message",
            )
        db.commit()

        # Admin should have 2 unread messages
        unread_count = messaging_service.get_unread_count(
            db=db,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
        )
        assert unread_count == 2

    def test_get_unread_count_zero(self, db, messaging_service, test_admin):
        """Test unread count when no messages."""
        unread_count = messaging_service.get_unread_count(
            db=db,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
        )
        assert unread_count == 0


@pytest.mark.unit
class TestMessagingServiceCloseReopen:
    """Test conversation close/reopen."""

    def test_close_conversation(
        self, db, messaging_service, test_conversation_admin_store, test_admin
    ):
        """Test closing a conversation."""
        conversation = messaging_service.close_conversation(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            closer_type=ParticipantType.ADMIN,
            closer_id=test_admin.id,
        )
        db.commit()

        assert conversation is not None
        assert conversation.is_closed is True
        assert conversation.closed_at is not None
        assert conversation.closed_by_type == ParticipantType.ADMIN
        assert conversation.closed_by_id == test_admin.id

        # Should have system message
        db.refresh(conversation)
        assert any(m.is_system_message and "closed" in m.content for m in conversation.messages)

    def test_reopen_conversation(
        self, db, messaging_service, closed_conversation, test_admin
    ):
        """Test reopening a closed conversation."""
        conversation = messaging_service.reopen_conversation(
            db=db,
            conversation_id=closed_conversation.id,
            opener_type=ParticipantType.ADMIN,
            opener_id=test_admin.id,
        )
        db.commit()

        assert conversation is not None
        assert conversation.is_closed is False
        assert conversation.closed_at is None
        assert conversation.closed_by_type is None
        assert conversation.closed_by_id is None


@pytest.mark.unit
class TestMessagingServiceParticipantInfo:
    """Test participant info retrieval."""

    def test_get_participant_info_admin(self, db, messaging_service, test_admin):
        """Test getting admin participant info."""
        info = messaging_service.get_participant_info(
            db=db,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
        )

        assert info is not None
        assert info["id"] == test_admin.id
        assert info["type"] == "admin"
        assert "email" in info

    def test_get_participant_info_customer(self, db, messaging_service, test_customer):
        """Test getting customer participant info."""
        info = messaging_service.get_participant_info(
            db=db,
            participant_type=ParticipantType.CUSTOMER,
            participant_id=test_customer.id,
        )

        assert info is not None
        assert info["id"] == test_customer.id
        assert info["type"] == "customer"
        assert info["name"] == "John Doe"

    def test_get_participant_info_not_found(self, db, messaging_service):
        """Test getting info for non-existent participant."""
        info = messaging_service.get_participant_info(
            db=db,
            participant_type=ParticipantType.ADMIN,
            participant_id=99999,
        )

        assert info is None


@pytest.mark.unit
class TestMessagingServiceNotificationPreferences:
    """Test notification preference updates."""

    def test_update_notification_preferences(
        self, db, messaging_service, test_conversation_admin_store, test_admin
    ):
        """Test updating notification preferences."""
        result = messaging_service.update_notification_preferences(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
            email_notifications=False,
            muted=True,
        )
        db.commit()

        assert result is True

        # Verify preferences updated
        participant = (
            db.query(ConversationParticipant)
            .filter(
                ConversationParticipant.conversation_id == test_conversation_admin_store.id,
                ConversationParticipant.participant_type == ParticipantType.ADMIN,
            )
            .first()
        )
        assert participant.email_notifications is False
        assert participant.muted is True

    def test_update_notification_preferences_no_changes(
        self, db, messaging_service, test_conversation_admin_store, test_admin
    ):
        """Test updating with no changes."""
        result = messaging_service.update_notification_preferences(
            db=db,
            conversation_id=test_conversation_admin_store.id,
            participant_type=ParticipantType.ADMIN,
            participant_id=test_admin.id,
        )

        assert result is False
