package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest;
import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse;
import dev.lions.unionflow.server.api.enums.communication.ConversationType;
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 dev.lions.unionflow.server.repository.OrganisationRepository;
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 ConversationService
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2026-03-20
 */
@QuarkusTest
@TestMethodOrder(MethodOrderer.DisplayName.class)
class ConversationServiceTest {

    @Inject
    ConversationService conversationService;

    @InjectMock
    ConversationRepository conversationRepository;

    @InjectMock
    MessageRepository messageRepository;

    @InjectSpy
    MembreRepository membreRepository;

    @InjectSpy
    OrganisationRepository organisationRepository;

    @InjectMock
    EntityManager entityManager;

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

    private Conversation mockConversation() {
        Conversation c = new Conversation();
        c.setId(UUID.randomUUID());
        c.setName("Test Conv");
        c.setIsMuted(false);
        c.setIsPinned(false);
        c.setIsArchived(false);
        c.setParticipants(new ArrayList<>());
        return c;
    }

    private Message mockMessage(Conversation conv) {
        Message msg = new Message();
        msg.setId(UUID.randomUUID());
        msg.setConversation(conv);
        Membre sender = new Membre();
        sender.setId(UUID.randomUUID());
        msg.setSender(sender);
        msg.setSenderName("Test Sender");
        msg.setContent("Hello");
        msg.setType(MessageType.INDIVIDUAL);
        msg.setStatus(MessageStatus.SENT);
        msg.setIsEdited(false);
        msg.setIsDeleted(false);
        return msg;
    }

    // -------------------------------------------------------------------------
    // getConversations
    // -------------------------------------------------------------------------

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

        when(conversationRepository.findByOrganisation(orgId)).thenReturn(List.of(conv));
        when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
        when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);

        List<ConversationResponse> result = conversationService.getConversations(membreId, orgId, false);

        assertThat(result).hasSize(1);
        verify(conversationRepository).findByOrganisation(orgId);
        verify(conversationRepository, never()).findByParticipant(any(), anyBoolean());
    }

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

        when(conversationRepository.findByParticipant(membreId, false)).thenReturn(List.of(conv));
        when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
        when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);

        List<ConversationResponse> result = conversationService.getConversations(membreId, null, false);

        assertThat(result).hasSize(1);
        verify(conversationRepository).findByParticipant(membreId, false);
        verify(conversationRepository, never()).findByOrganisation(any());
    }

    @Test
    @DisplayName("getConversations_includesLastMessageAndUnread")
    void getConversations_includesLastMessageAndUnread() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();
        Message lastMsg = mockMessage(conv);

        when(conversationRepository.findByParticipant(membreId, true)).thenReturn(List.of(conv));
        when(messageRepository.findLastByConversation(conv.getId())).thenReturn(lastMsg);
        when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(3L);

        List<ConversationResponse> result = conversationService.getConversations(membreId, null, true);

        assertThat(result).hasSize(1);
        ConversationResponse response = result.get(0);
        assertThat(response.lastMessage()).isNotNull();
        assertThat(response.lastMessage().content()).isEqualTo("Hello");
        assertThat(response.unreadCount()).isEqualTo(3);
    }

    // -------------------------------------------------------------------------
    // getConversationById
    // -------------------------------------------------------------------------

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

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

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

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

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
        when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);

        ConversationResponse response = conversationService.getConversationById(conv.getId(), membreId);

        assertThat(response).isNotNull();
        assertThat(response.id()).isEqualTo(conv.getId());
        assertThat(response.name()).isEqualTo("Test Conv");
    }

    // -------------------------------------------------------------------------
    // createConversation
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("createConversation_withoutOrg_success")
    void createConversation_withoutOrg_success() {
        UUID creatorId = UUID.randomUUID();
        Membre creator = new Membre();
        creator.setId(creatorId);

        CreateConversationRequest request = CreateConversationRequest.builder()
                .name("New Conv")
                .description("desc")
                .type(ConversationType.GROUP)
                .participantIds(new ArrayList<>())
                .organisationId(null)
                .build();

        when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
        when(messageRepository.findLastByConversation(any())).thenReturn(null);
        when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);

        ConversationResponse response = conversationService.createConversation(request, creatorId);

        assertThat(response).isNotNull();
        assertThat(response.name()).isEqualTo("New Conv");
        verify(conversationRepository).persist(any(Conversation.class));
        verify(entityManager, never()).find(eq(Organisation.class), any());
    }

    @Test
    @DisplayName("createConversation_withOrg_setsOrganisation")
    void createConversation_withOrg_setsOrganisation() {
        UUID creatorId = UUID.randomUUID();
        UUID orgId = UUID.randomUUID();
        Membre creator = new Membre();
        creator.setId(creatorId);
        Organisation org = new Organisation();
        org.setId(orgId);

        CreateConversationRequest request = CreateConversationRequest.builder()
                .name("Org Conv")
                .description(null)
                .type(ConversationType.BROADCAST)
                .participantIds(new ArrayList<>())
                .organisationId(orgId)
                .build();

        when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
        when(entityManager.find(Organisation.class, orgId)).thenReturn(org);
        when(messageRepository.findLastByConversation(any())).thenReturn(null);
        when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);

        ConversationResponse response = conversationService.createConversation(request, creatorId);

        assertThat(response).isNotNull();
        assertThat(response.organisationId()).isEqualTo(orgId);
        verify(entityManager).find(Organisation.class, orgId);
    }

    @Test
    @DisplayName("createConversation_creatorNotInList_addsCreator")
    void createConversation_creatorNotInList_addsCreator() {
        UUID creatorId = UUID.randomUUID();
        UUID participant1Id = UUID.randomUUID();
        Membre creator = new Membre();
        creator.setId(creatorId);
        Membre participant1 = new Membre();
        participant1.setId(participant1Id);

        CreateConversationRequest request = CreateConversationRequest.builder()
                .name("Conv")
                .description(null)
                .type(ConversationType.INDIVIDUAL)
                .participantIds(new ArrayList<>(List.of(participant1Id)))
                .organisationId(null)
                .build();

        when(entityManager.find(Membre.class, participant1Id)).thenReturn(participant1);
        when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
        when(messageRepository.findLastByConversation(any())).thenReturn(null);
        when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);

        ConversationResponse response = conversationService.createConversation(request, creatorId);

        // Creator + participant1 = 2 participants
        assertThat(response.participantIds()).hasSize(2);
        assertThat(response.participantIds()).contains(creatorId, participant1Id);
    }

    @Test
    @DisplayName("createConversation_creatorAlreadyInList_doesNotDuplicate")
    void createConversation_creatorAlreadyInList_doesNotDuplicate() {
        UUID creatorId = UUID.randomUUID();
        Membre creator = new Membre();
        creator.setId(creatorId);

        CreateConversationRequest request = CreateConversationRequest.builder()
                .name("Conv")
                .description(null)
                .type(ConversationType.INDIVIDUAL)
                .participantIds(new ArrayList<>(List.of(creatorId)))
                .organisationId(null)
                .build();

        // findById(creatorId) appelé 2 fois: une pour le participant, une pour le créateur
        when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
        when(messageRepository.findLastByConversation(any())).thenReturn(null);
        when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);

        ConversationResponse response = conversationService.createConversation(request, creatorId);

        // Le créateur ne doit pas être dupliqué
        assertThat(response.participantIds()).hasSize(1);
        assertThat(response.participantIds()).containsExactly(creatorId);
    }

    @Test
    @DisplayName("createConversation_participantNotFound_filtersNull")
    void createConversation_participantNotFound_filtersNull() {
        UUID creatorId = UUID.randomUUID();
        UUID unknownId = UUID.randomUUID();
        Membre creator = new Membre();
        creator.setId(creatorId);

        CreateConversationRequest request = CreateConversationRequest.builder()
                .name("Conv")
                .description(null)
                .type(ConversationType.GROUP)
                .participantIds(new ArrayList<>(List.of(unknownId)))
                .organisationId(null)
                .build();

        when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
        when(messageRepository.findLastByConversation(any())).thenReturn(null);
        when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);

        ConversationResponse response = conversationService.createConversation(request, creatorId);

        // unknownId est filtré, seul le créateur reste
        assertThat(response.participantIds()).hasSize(1);
        assertThat(response.participantIds()).containsExactly(creatorId);
    }

    @Test
    @DisplayName("createConversation_creatorNotFound_doesNotAddCreator (creator == null → L97 false)")
    void createConversation_creatorNull_doesNotAddCreator() {
        UUID creatorId = UUID.randomUUID();

        CreateConversationRequest request = CreateConversationRequest.builder()
                .name("No Creator Conv")
                .description(null)
                .type(ConversationType.GROUP)
                .participantIds(new ArrayList<>())
                .organisationId(null)
                .build();

        // creator == null → condition L97: creator != null = false → pas d'ajout du créateur
        when(entityManager.find(Membre.class, creatorId)).thenReturn(null);
        when(messageRepository.findLastByConversation(any())).thenReturn(null);
        when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);

        ConversationResponse response = conversationService.createConversation(request, creatorId);

        assertThat(response).isNotNull();
        // Aucun participant car creator introuvable et liste vide
        assertThat(response.participantIds()).isEmpty();
    }

    // -------------------------------------------------------------------------
    // archiveConversation
    // -------------------------------------------------------------------------

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

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

        assertThatThrownBy(() -> conversationService.archiveConversation(convId, membreId, true))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining("Conversation non trouvée");
    }

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

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));

        conversationService.archiveConversation(conv.getId(), membreId, true);

        assertThat(conv.getIsArchived()).isTrue();
        verify(conversationRepository).persist(conv);
    }

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

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));

        conversationService.archiveConversation(conv.getId(), membreId, false);

        assertThat(conv.getIsArchived()).isFalse();
        verify(conversationRepository).persist(conv);
    }

    // -------------------------------------------------------------------------
    // markAsRead
    // -------------------------------------------------------------------------

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

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

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

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

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        when(messageRepository.markAllAsReadByConversationAndMember(conv.getId(), membreId)).thenReturn(5);

        conversationService.markAsRead(conv.getId(), membreId);

        verify(messageRepository).markAllAsReadByConversationAndMember(conv.getId(), membreId);
    }

    // -------------------------------------------------------------------------
    // toggleMute
    // -------------------------------------------------------------------------

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

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

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

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

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));

        conversationService.toggleMute(conv.getId(), membreId);

        assertThat(conv.getIsMuted()).isTrue();
    }

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

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));

        conversationService.toggleMute(conv.getId(), membreId);

        assertThat(conv.getIsMuted()).isFalse();
    }

    // -------------------------------------------------------------------------
    // togglePin
    // -------------------------------------------------------------------------

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

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

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

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

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));

        conversationService.togglePin(conv.getId(), membreId);

        assertThat(conv.getIsPinned()).isTrue();
    }

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

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));

        conversationService.togglePin(conv.getId(), membreId);

        assertThat(conv.getIsPinned()).isFalse();
    }

    // -------------------------------------------------------------------------
    // convertToResponse (via getConversationById)
    // -------------------------------------------------------------------------

    @Test
    @DisplayName("convertToResponse_withLastMessage_includesMessage")
    void convertToResponse_withLastMessage_includesMessage() {
        UUID membreId = UUID.randomUUID();
        Conversation conv = mockConversation();
        Message lastMsg = mockMessage(conv);
        lastMsg.setContent("Dernier message");

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        when(messageRepository.findLastByConversation(conv.getId())).thenReturn(lastMsg);
        when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);

        ConversationResponse response = conversationService.getConversationById(conv.getId(), membreId);

        assertThat(response.lastMessage()).isNotNull();
        assertThat(response.lastMessage().content()).isEqualTo("Dernier message");
        assertThat(response.lastMessage().id()).isEqualTo(lastMsg.getId());
        assertThat(response.lastMessage().senderId()).isEqualTo(lastMsg.getSender().getId());
    }

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

        when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
        when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
        when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);

        ConversationResponse response = conversationService.getConversationById(conv.getId(), membreId);

        assertThat(response.lastMessage()).isNull();
    }
}
