package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest;
import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
import dev.lions.unionflow.server.api.enums.communication.MessagePriority;
import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
import dev.lions.unionflow.server.api.enums.communication.MessageType;
import dev.lions.unionflow.server.entity.Conversation;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Message;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.ConversationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.MessageRepository;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectSpy;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.ws.rs.NotFoundException;
import org.junit.jupiter.api.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * Tests unitaires pour MessageService
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2026-03-20
 */
@QuarkusTest
@TestMethodOrder(MethodOrderer.DisplayName.class)
class MessageServiceTest {

    @Inject
    MessageService messageService;

    @InjectSpy
    MessageRepository messageRepository;

    @InjectMock
    ConversationRepository conversationRepository;

    @InjectSpy
    MembreRepository membreRepository;

    @InjectMock
    EntityManager entityManager;

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private Conversation mockConversation() {
        Conversation c = new Conversation();
        c.setId(UUID.randomUUID());
        c.setParticipants(new ArrayList<>());
        return c;
    }

    private Membre mockMembre(UUID id) {
        Membre m = new Membre();
        m.setId(id);
        m.setPrenom("Jean");
        m.setNom("Dupont");
        return m;
    }

    private Message mockMessage(UUID senderId) {
        Message msg = new Message();
        msg.setId(UUID.randomUUID());
        Conversation conv = mockConversation();
        msg.setConversation(conv);
        Membre sender = mockMembre(senderId);
        msg.setSender(sender);
        msg.setSenderName("Jean Dupont");
        msg.setContent("Hello");
        msg.setType(MessageType.INDIVIDUAL);
        msg.setStatus(MessageStatus.SENT);
        msg.setPriority(MessagePriority.NORMAL);
        msg.setIsEdited(false);
        msg.setIsDeleted(false);
        return msg;
    }

    // -------------------------------------------------------------------------
    // getMessages
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("getMessages_conversationNotFound_throwsNotFound")
    void getMessages_conversationNotFound_throwsNotFound() {
        UUID convId = UUID.randomUUID();
        UUID membreId = UUID.randomUUID();

        when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> messageService.getMessages(convId, membreId, 20))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Conversation non trouvée");
    }

    @Test
    @DisplayName("getMessages_found_returnsList")
    void getMessages_found_returnsList() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();
        Message msg = mockMessage(UUID.randomUUID());
        msg.setConversation(conv);

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);

        List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);

        assertThat(result).hasSize(1);
        assertThat(result.get(0).content()).isEqualTo("Hello");
    }

    // -------------------------------------------------------------------------
    // sendMessage
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("sendMessage_conversationNotFound_throwsNotFound")
    void sendMessage_conversationNotFound_throwsNotFound() {
        UUID senderId = UUID.randomUUID();
        UUID convId = UUID.randomUUID();

        SendMessageRequest request = SendMessageRequest.builder()
                .conversationId(convId)
                .content("test")
                .build();

        when(conversationRepository.findByIdAndParticipant(convId, senderId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> messageService.sendMessage(request, senderId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Conversation non trouvée");
    }

    @Test
    @DisplayName("sendMessage_senderNotFound_throwsNotFound")
    void sendMessage_senderNotFound_throwsNotFound() {
        UUID senderId = UUID.randomUUID();
        Conversation conv = mockConversation();

        SendMessageRequest request = SendMessageRequest.builder()
                .conversationId(conv.getId())
                .content("test")
                .build();

        when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
        // entityManager.find returns null by default → findById(senderId) returns null → service throws

        assertThatThrownBy(() -> messageService.sendMessage(request, senderId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Expéditeur non trouvé");
    }

    @Test
    @DisplayName("sendMessage_withDefaultTypeAndPriority_success")
    void sendMessage_withDefaultTypeAndPriority_success() {
        UUID senderId = UUID.randomUUID();
        Conversation conv = mockConversation();
        Membre sender = mockMembre(senderId);

        SendMessageRequest request = SendMessageRequest.builder()
                .conversationId(conv.getId())
                .content("Bonjour")
                .type(null)
                .priority(null)
                .recipientIds(null)
                .recipientRoles(null)
                .attachments(null)
                .build();

        when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
        when(entityManager.find(Membre.class, senderId)).thenReturn(sender);

        MessageResponse response = messageService.sendMessage(request, senderId);

        assertThat(response).isNotNull();
        assertThat(response.content()).isEqualTo("Bonjour");
        assertThat(response.type()).isEqualTo(MessageType.INDIVIDUAL);
        assertThat(response.priority()).isEqualTo(MessagePriority.NORMAL);
        assertThat(response.senderName()).isEqualTo("Jean Dupont");
        verify(messageRepository).persist(any(Message.class));
        verify(conversationRepository).persist(conv);
    }

    @Test
    @DisplayName("sendMessage_withExplicitType_usesType")
    void sendMessage_withExplicitType_usesType() {
        UUID senderId = UUID.randomUUID();
        Conversation conv = mockConversation();
        Membre sender = mockMembre(senderId);

        SendMessageRequest request = SendMessageRequest.builder()
                .conversationId(conv.getId())
                .content("Broadcast!")
                .type(MessageType.BROADCAST)
                .priority(MessagePriority.HIGH)
                .recipientIds(null)
                .recipientRoles(null)
                .attachments(null)
                .build();

        when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
        when(entityManager.find(Membre.class, senderId)).thenReturn(sender);

        MessageResponse response = messageService.sendMessage(request, senderId);

        assertThat(response.type()).isEqualTo(MessageType.BROADCAST);
        assertThat(response.priority()).isEqualTo(MessagePriority.HIGH);
    }

    @Test
    @DisplayName("sendMessage_withRecipientIds_setsCSV")
    void sendMessage_withRecipientIds_setsCSV() {
        UUID senderId = UUID.randomUUID();
        UUID recipient1 = UUID.randomUUID();
        UUID recipient2 = UUID.randomUUID();
        Conversation conv = mockConversation();
        Membre sender = mockMembre(senderId);

        SendMessageRequest request = SendMessageRequest.builder()
                .conversationId(conv.getId())
                .content("Targeted")
                .type(MessageType.TARGETED)
                .priority(null)
                .recipientIds(List.of(recipient1, recipient2))
                .recipientRoles(null)
                .attachments(null)
                .build();

        when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
        when(entityManager.find(Membre.class, senderId)).thenReturn(sender);

        MessageResponse response = messageService.sendMessage(request, senderId);

        assertThat(response.recipientIds()).isNotNull();
        assertThat(response.recipientIds()).containsExactlyInAnyOrder(recipient1, recipient2);
    }

    @Test
    @DisplayName("sendMessage_withRecipientRoles_setsCSV")
    void sendMessage_withRecipientRoles_setsCSV() {
        UUID senderId = UUID.randomUUID();
        Conversation conv = mockConversation();
        Membre sender = mockMembre(senderId);

        SendMessageRequest request = SendMessageRequest.builder()
                .conversationId(conv.getId())
                .content("Role msg")
                .type(null)
                .priority(null)
                .recipientIds(null)
                .recipientRoles(List.of("ADMIN", "TRESORIER"))
                .attachments(null)
                .build();

        when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
        when(entityManager.find(Membre.class, senderId)).thenReturn(sender);

        MessageResponse response = messageService.sendMessage(request, senderId);

        assertThat(response.recipientRoles()).isNotNull();
        assertThat(response.recipientRoles()).containsExactly("ADMIN", "TRESORIER");
    }

    @Test
    @DisplayName("sendMessage_withAttachments_setsCSV")
    void sendMessage_withAttachments_setsCSV() {
        UUID senderId = UUID.randomUUID();
        Conversation conv = mockConversation();
        Membre sender = mockMembre(senderId);

        SendMessageRequest request = SendMessageRequest.builder()
                .conversationId(conv.getId())
                .content("Msg avec PJ")
                .type(null)
                .priority(null)
                .recipientIds(null)
                .recipientRoles(null)
                .attachments(List.of("https://cdn.example.com/doc1.pdf", "https://cdn.example.com/img1.png"))
                .build();

        when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
        when(entityManager.find(Membre.class, senderId)).thenReturn(sender);

        MessageResponse response = messageService.sendMessage(request, senderId);

        assertThat(response.attachments()).isNotNull();
        assertThat(response.attachments()).containsExactly(
                "https://cdn.example.com/doc1.pdf",
                "https://cdn.example.com/img1.png"
        );
    }

    @Test
    @DisplayName("sendMessage_noRecipientsNoRolesNoAttachments_noCSV")
    void sendMessage_noRecipientsNoRolesNoAttachments_noCSV() {
        UUID senderId = UUID.randomUUID();
        Conversation conv = mockConversation();
        Membre sender = mockMembre(senderId);

        SendMessageRequest request = SendMessageRequest.builder()
                .conversationId(conv.getId())
                .content("Simple")
                .type(null)
                .priority(null)
                .recipientIds(new ArrayList<>())
                .recipientRoles(new ArrayList<>())
                .attachments(new ArrayList<>())
                .build();

        when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
        when(entityManager.find(Membre.class, senderId)).thenReturn(sender);

        MessageResponse response = messageService.sendMessage(request, senderId);

        assertThat(response.recipientIds()).isNull();
        assertThat(response.recipientRoles()).isNull();
        assertThat(response.attachments()).isNull();
    }

    // -------------------------------------------------------------------------
    // editMessage
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("editMessage_notFound_throwsNotFound")
    void editMessage_notFound_throwsNotFound() {
        UUID messageId = UUID.randomUUID();
        UUID senderId = UUID.randomUUID();

        // entityManager.find returns null by default → findById(messageId) returns null → service throws

        assertThatThrownBy(() -> messageService.editMessage(messageId, senderId, "nouveau contenu"))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Message non trouvé");
    }

    @Test
    @DisplayName("editMessage_wrongSender_throwsIllegalState")
    void editMessage_wrongSender_throwsIllegalState() {
        UUID realSenderId = UUID.randomUUID();
        UUID wrongSenderId = UUID.randomUUID();
        Message msg = mockMessage(realSenderId);

        when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);

        assertThatThrownBy(() -> messageService.editMessage(msg.getId(), wrongSenderId, "contenu modifié"))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("propres messages");
    }

    @Test
    @DisplayName("editMessage_success_updatesContent")
    void editMessage_success_updatesContent() {
        UUID senderId = UUID.randomUUID();
        Message msg = mockMessage(senderId);
        msg.setIsEdited(false);

        when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);

        MessageResponse response = messageService.editMessage(msg.getId(), senderId, "Contenu édité");

        assertThat(msg.getContent()).isEqualTo("Contenu édité");
        assertThat(msg.getIsEdited()).isTrue();
        assertThat(msg.getEditedAt()).isNotNull();
        verify(messageRepository).persist(msg);
        assertThat(response.content()).isEqualTo("Contenu édité");
        assertThat(response.isEdited()).isTrue();
    }

    // -------------------------------------------------------------------------
    // deleteMessage
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("deleteMessage_notFound_throwsNotFound")
    void deleteMessage_notFound_throwsNotFound() {
        UUID messageId = UUID.randomUUID();
        UUID senderId = UUID.randomUUID();

        // entityManager.find returns null by default → findById(messageId) returns null → service throws

        assertThatThrownBy(() -> messageService.deleteMessage(messageId, senderId))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Message non trouvé");
    }

    @Test
    @DisplayName("deleteMessage_wrongSender_throwsIllegalState")
    void deleteMessage_wrongSender_throwsIllegalState() {
        UUID realSenderId = UUID.randomUUID();
        UUID wrongSenderId = UUID.randomUUID();
        Message msg = mockMessage(realSenderId);

        when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);

        assertThatThrownBy(() -> messageService.deleteMessage(msg.getId(), wrongSenderId))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("propres messages");
    }

    @Test
    @DisplayName("deleteMessage_success_softDeletes")
    void deleteMessage_success_softDeletes() {
        UUID senderId = UUID.randomUUID();
        Message msg = mockMessage(senderId);
        msg.setIsDeleted(false);
        msg.setContent("Contenu original");

        when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);

        messageService.deleteMessage(msg.getId(), senderId);

        assertThat(msg.getIsDeleted()).isTrue();
        assertThat(msg.getContent()).isEqualTo("[Message supprimé]");
        verify(messageRepository).persist(msg);
    }

    // -------------------------------------------------------------------------
    // convertToResponse (via getMessages)
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("convertToResponse_withRecipientIds_parsesCsv")
    void convertToResponse_withRecipientIds_parsesCsv() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();
        UUID r1 = UUID.randomUUID();
        UUID r2 = UUID.randomUUID();

        Message msg = mockMessage(UUID.randomUUID());
        msg.setConversation(conv);
        msg.setRecipientIds(r1 + "," + r2);

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);

        List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);

        assertThat(result).hasSize(1);
        assertThat(result.get(0).recipientIds()).containsExactlyInAnyOrder(r1, r2);
    }

    @Test
    @DisplayName("convertToResponse_withRecipientRoles_parsesCsv")
    void convertToResponse_withRecipientRoles_parsesCsv() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();

        Message msg = mockMessage(UUID.randomUUID());
        msg.setConversation(conv);
        msg.setRecipientRoles("ADMIN,SECRETAIRE");

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);

        List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);

        assertThat(result.get(0).recipientRoles()).containsExactly("ADMIN", "SECRETAIRE");
    }

    @Test
    @DisplayName("convertToResponse_withAttachments_parsesCsv")
    void convertToResponse_withAttachments_parsesCsv() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();

        Message msg = mockMessage(UUID.randomUUID());
        msg.setConversation(conv);
        msg.setAttachments("https://cdn.example.com/a.pdf,https://cdn.example.com/b.png");

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);

        List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);

        assertThat(result.get(0).attachments()).containsExactly(
                "https://cdn.example.com/a.pdf",
                "https://cdn.example.com/b.png"
        );
    }

    @Test
    @DisplayName("convertToResponse_noRecipients_nullFields")
    void convertToResponse_noRecipients_nullFields() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();

        Message msg = mockMessage(UUID.randomUUID());
        msg.setConversation(conv);
        // recipientIds, recipientRoles et attachments sont null par défaut

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);

        List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);

        MessageResponse response = result.get(0);
        assertThat(response.recipientIds()).isNull();
        assertThat(response.recipientRoles()).isNull();
        assertThat(response.attachments()).isNull();
    }

    @Test
    @DisplayName("convertToResponse_withOrganisation_setsOrgId")
    void convertToResponse_withOrganisation_setsOrgId() {
        UUID membreId = UUID.randomUUID();
        UUID orgId = UUID.randomUUID();
        Conversation conv = mockConversation();
        Organisation org = new Organisation();
        org.setId(orgId);
        conv.setOrganisation(org);

        Message msg = mockMessage(UUID.randomUUID());
        msg.setConversation(conv);
        msg.setOrganisation(org);

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);

        List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);

        assertThat(result.get(0).organisationId()).isEqualTo(orgId);
    }

    @Test
    @DisplayName("convertToResponse_noOrganisation_nullOrgId")
    void convertToResponse_noOrganisation_nullOrgId() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();
        // organisation est null

        Message msg = mockMessage(UUID.randomUUID());
        msg.setConversation(conv);
        msg.setOrganisation(null);

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);

        List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);

        assertThat(result.get(0).organisationId()).isNull();
    }

    // -------------------------------------------------------------------------
    // convertToResponse — branches isEmpty() (non-null mais vide)
    // L163: recipientIds != null && !isEmpty() → false (empty string → isEmpty = true)
    // L172: recipientRoles != null && !isEmpty() → false
    // L178: attachments != null && !isEmpty() → false
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("convertToResponse_recipientIdsEmptyString_returnsNullRecipientIds (L163 false)")
    void convertToResponse_recipientIdsEmptyString_returnsNullRecipientIds() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();

        Message msg = mockMessage(UUID.randomUUID());
        msg.setConversation(conv);
        // non-null mais vide → L163: isEmpty() = true → condition false → recipientIds = null
        msg.setRecipientIds("");

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);

        List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);

        assertThat(result.get(0).recipientIds()).isNull();
    }

    @Test
    @DisplayName("convertToResponse_recipientRolesEmptyString_returnsNullRoles (L172 false)")
    void convertToResponse_recipientRolesEmptyString_returnsNullRoles() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();

        Message msg = mockMessage(UUID.randomUUID());
        msg.setConversation(conv);
        msg.setRecipientRoles(""); // non-null mais vide → L172 false → roles = null

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);

        List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);

        assertThat(result.get(0).recipientRoles()).isNull();
    }

    @Test
    @DisplayName("convertToResponse_attachmentsEmptyString_returnsNullAttachments (L178 false)")
    void convertToResponse_attachmentsEmptyString_returnsNullAttachments() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();

        Message msg = mockMessage(UUID.randomUUID());
        msg.setConversation(conv);
        msg.setAttachments(""); // non-null mais vide → L178 false → attachments = null

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);

        List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);

        assertThat(result.get(0).attachments()).isNull();
    }
}
