package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.messagerie.request.BloquerMembreRequest;
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationDirecteRequest;
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationRoleRequest;
import dev.lions.unionflow.server.api.dto.messagerie.request.EnvoyerMessageRequest;
import dev.lions.unionflow.server.api.dto.messagerie.request.MettreAJourPolitiqueRequest;
import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse;
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse;
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse;
import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse;
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication;
import dev.lions.unionflow.server.entity.ContactPolicy;
import dev.lions.unionflow.server.entity.Conversation;
import dev.lions.unionflow.server.entity.ConversationParticipant;
import dev.lions.unionflow.server.entity.MemberBlock;
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.ContactPolicyRepository;
import dev.lions.unionflow.server.repository.ConversationParticipantRepository;
import dev.lions.unionflow.server.repository.ConversationRepository;
import dev.lions.unionflow.server.repository.MemberBlockRepository;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.MessageRepository;
import dev.lions.unionflow.server.messaging.KafkaEventProducer;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.NotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Tests unitaires du MessagingService avec mocks.
 */
@QuarkusTest
@DisplayName("MessagingService")
class MessagingServiceTest {

    @Inject
    MessagingService messagingService;

    @InjectMock ConversationRepository conversationRepository;
    @InjectMock ConversationParticipantRepository participantRepository;
    @InjectMock MessageRepository messageRepository;
    @InjectMock ContactPolicyRepository contactPolicyRepository;
    @InjectMock MemberBlockRepository memberBlockRepository;
    @InjectMock MembreRepository membreRepository;
    @InjectMock MembreOrganisationRepository membreOrganisationRepository;
    @InjectMock KafkaEventProducer kafkaEventProducer;
    @InjectMock SecurityIdentity securityIdentity;

    private static final UUID MEMBRE_ID       = UUID.fromString("00000000-0000-0000-0000-000000000001");
    private static final UUID DESTINATAIRE_ID = UUID.fromString("00000000-0000-0000-0000-000000000002");
    private static final UUID ORG_ID          = UUID.fromString("00000000-0000-0000-0000-000000000003");
    private static final UUID CONV_ID         = UUID.fromString("00000000-0000-0000-0000-000000000004");
    private static final UUID MESSAGE_ID      = UUID.fromString("00000000-0000-0000-0000-000000000005");

    @BeforeEach
    void setUp() {
        // Mock du membre connecté
        io.quarkus.security.runtime.QuarkusPrincipal principal =
            new io.quarkus.security.runtime.QuarkusPrincipal("membre@test.com");
        when(securityIdentity.getPrincipal()).thenReturn(principal);

        Membre moi = newMembre(MEMBRE_ID, "membre@test.com", "Alpha", "Diallo");
        when(membreRepository.find("email", "membre@test.com"))
            .thenReturn(io.quarkus.hibernate.orm.panache.PanacheQuery.class.cast(
                mockSingleResultQuery(moi)));
    }

    // ── getMesConversations ───────────────────────────────────────────────────

    @Test
    @DisplayName("getMesConversations — retourne liste vide")
    void getMesConversations_returnsEmpty() {
        when(conversationRepository.findByMembreId(MEMBRE_ID))
            .thenReturn(Collections.emptyList());

        List<ConversationSummaryResponse> result = messagingService.getMesConversations();

        assertThat(result).isNotNull().isEmpty();
    }

    @Test
    @DisplayName("getMesConversations — retourne la liste des conversations")
    void getMesConversations_returnsList() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        when(conversationRepository.findByMembreId(MEMBRE_ID)).thenReturn(List.of(conv));
        when(messageRepository.findDernierMessage(CONV_ID)).thenReturn(Optional.empty());
        when(messageRepository.countNonLus(CONV_ID, MEMBRE_ID)).thenReturn(0L);
        when(participantRepository.findByConversation(CONV_ID)).thenReturn(Collections.emptyList());

        List<ConversationSummaryResponse> result = messagingService.getMesConversations();

        assertThat(result).hasSize(1);
        assertThat(result.get(0).getTypeConversation()).isEqualTo("DIRECTE");
    }

    // ── getConversation ───────────────────────────────────────────────────────

    @Test
    @DisplayName("getConversation — notFound → NotFoundException")
    void getConversation_notFound() {
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> messagingService.getConversation(CONV_ID))
            .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("getConversation — non participant → ForbiddenException")
    void getConversation_nonParticipant() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
        when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(false);

        assertThatThrownBy(() -> messagingService.getConversation(CONV_ID))
            .isInstanceOf(ForbiddenException.class);
    }

    @Test
    @DisplayName("getConversation — success → retourne ConversationResponse")
    void getConversation_success() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
        when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);
        when(participantRepository.findByConversation(CONV_ID)).thenReturn(Collections.emptyList());
        when(messageRepository.findByConversationPagine(eq(CONV_ID), eq(0), anyInt()))
            .thenReturn(Collections.emptyList());
        when(messageRepository.countNonLus(CONV_ID, MEMBRE_ID)).thenReturn(2L);

        ConversationResponse result = messagingService.getConversation(CONV_ID);

        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(CONV_ID);
        assertThat(result.getNonLus()).isEqualTo(2L);
    }

    // ── archiverConversation ──────────────────────────────────────────────────

    @Test
    @DisplayName("archiverConversation — success → statut ARCHIVEE")
    void archiverConversation_success() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
        when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);
        when(participantRepository.findByConversation(CONV_ID)).thenReturn(Collections.emptyList());
        when(messageRepository.findByConversationPagine(eq(CONV_ID), eq(0), anyInt()))
            .thenReturn(Collections.emptyList());
        when(messageRepository.countNonLus(CONV_ID, MEMBRE_ID)).thenReturn(0L);

        ConversationResponse result = messagingService.archiverConversation(CONV_ID);

        assertThat(result.getStatut()).isEqualTo("ARCHIVEE");
    }

    // ── envoyerMessage ────────────────────────────────────────────────────────

    @Test
    @DisplayName("envoyerMessage — conversation non trouvée → NotFoundException")
    void envoyerMessage_notFound() {
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.empty());

        EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
            .typeMessage("TEXTE").contenu("Bonjour").build();

        assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
            .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("envoyerMessage — non participant → ForbiddenException")
    void envoyerMessage_nonParticipant() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
        when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(false);

        EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
            .typeMessage("TEXTE").contenu("Bonjour").build();

        assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
            .isInstanceOf(ForbiddenException.class);
    }

    @Test
    @DisplayName("envoyerMessage — conversation archivée → BadRequestException")
    void envoyerMessage_archived() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        conv.setStatut(StatutConversation.ARCHIVEE);
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
        when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);

        EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
            .typeMessage("TEXTE").contenu("Bonjour").build();

        assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
            .isInstanceOf(BadRequestException.class);
    }

    @Test
    @DisplayName("envoyerMessage — TEXTE sans contenu → BadRequestException")
    void envoyerMessage_texteSansContenu() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
        when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);

        EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
            .typeMessage("TEXTE").contenu("").build();

        assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
            .isInstanceOf(BadRequestException.class);
    }

    @Test
    @DisplayName("envoyerMessage — VOCAL sans urlFichier → BadRequestException")
    void envoyerMessage_vocalSansUrl() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
        when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);

        EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
            .typeMessage("VOCAL").dureeAudio(30).build();

        assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
            .isInstanceOf(BadRequestException.class);
    }

    @Test
    @DisplayName("envoyerMessage — VOCAL sans dureeAudio → BadRequestException")
    void envoyerMessage_vocalSansDuree() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
        when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);

        EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
            .typeMessage("VOCAL").urlFichier("https://example.com/audio.opus").build();

        assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
            .isInstanceOf(BadRequestException.class);
    }

    // ── getMessages ───────────────────────────────────────────────────────────

    @Test
    @DisplayName("getMessages — success → retourne liste de messages")
    void getMessages_success() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        Message msg = buildMessage(conv);
        when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
        when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);
        when(messageRepository.findByConversationPagine(eq(CONV_ID), eq(0), anyInt()))
            .thenReturn(List.of(msg));

        List<MessageResponse> result = messagingService.getMessages(CONV_ID, 0);

        assertThat(result).hasSize(1);
        assertThat(result.get(0).getTypeMessage()).isEqualTo("TEXTE");
    }

    // ── marquerConversationLue ────────────────────────────────────────────────

    @Test
    @DisplayName("marquerConversationLue — participant trouvé → luJusqua mis à jour")
    void marquerConversationLue_success() {
        ConversationParticipant participant = new ConversationParticipant();
        participant.setMembre(newMembre(MEMBRE_ID, "membre@test.com", "Alpha", "Diallo"));
        when(participantRepository.findParticipant(CONV_ID, MEMBRE_ID))
            .thenReturn(Optional.of(participant));

        messagingService.marquerConversationLue(CONV_ID);

        assertThat(participant.getLuJusqua()).isNotNull();
    }

    @Test
    @DisplayName("marquerConversationLue — participant absent → no-op")
    void marquerConversationLue_noParticipant() {
        when(participantRepository.findParticipant(CONV_ID, MEMBRE_ID))
            .thenReturn(Optional.empty());

        // ne lève pas d'exception
        messagingService.marquerConversationLue(CONV_ID);
    }

    // ── supprimerMessage ──────────────────────────────────────────────────────

    @Test
    @DisplayName("supprimerMessage — message non trouvé → NotFoundException")
    void supprimerMessage_notFound() {
        when(messageRepository.findMessageById(MESSAGE_ID)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> messagingService.supprimerMessage(CONV_ID, MESSAGE_ID))
            .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("supprimerMessage — message dans autre conversation → NotFoundException")
    void supprimerMessage_wrongConversation() {
        Conversation autreConv = buildConversation(TypeConversation.DIRECTE);
        autreConv.setId(UUID.randomUUID()); // ID différent de CONV_ID
        Message msg = buildMessage(autreConv);
        when(messageRepository.findMessageById(MESSAGE_ID)).thenReturn(Optional.of(msg));

        assertThatThrownBy(() -> messagingService.supprimerMessage(CONV_ID, MESSAGE_ID))
            .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("supprimerMessage — pas l'auteur → ForbiddenException")
    void supprimerMessage_notAuthor() {
        Conversation conv = buildConversation(TypeConversation.DIRECTE);
        Membre autreExpedireur = newMembre(DESTINATAIRE_ID, "other@test.com", "Beta", "Koné");
        Message msg = new Message();
        msg.setId(MESSAGE_ID);
        msg.setConversation(conv);
        msg.setExpediteur(autreExpedireur);
        msg.setTypeMessage(TypeContenu.TEXTE);
        msg.setContenu("Message d'un autre");
        when(messageRepository.findMessageById(MESSAGE_ID)).thenReturn(Optional.of(msg));

        assertThatThrownBy(() -> messagingService.supprimerMessage(CONV_ID, MESSAGE_ID))
            .isInstanceOf(ForbiddenException.class);
    }

    // ── bloquerMembre ─────────────────────────────────────────────────────────

    @Test
    @DisplayName("bloquerMembre — soi-même → BadRequestException")
    void bloquerMembre_soiMeme() {
        BloquerMembreRequest req = BloquerMembreRequest.builder()
            .membreABloquerId(MEMBRE_ID)
            .organisationId(ORG_ID)
            .build();

        assertThatThrownBy(() -> messagingService.bloquerMembre(req))
            .isInstanceOf(BadRequestException.class);
    }

    @Test
    @DisplayName("bloquerMembre — membre introuvable → NotFoundException")
    void bloquerMembre_destinataireInconnu() {
        when(membreRepository.findById(DESTINATAIRE_ID)).thenReturn(null);

        BloquerMembreRequest req = BloquerMembreRequest.builder()
            .membreABloquerId(DESTINATAIRE_ID)
            .organisationId(ORG_ID)
            .build();

        assertThatThrownBy(() -> messagingService.bloquerMembre(req))
            .isInstanceOf(NotFoundException.class);
    }

    @Test
    @DisplayName("bloquerMembre — déjà bloqué → BadRequestException")
    void bloquerMembre_dejaBloque() {
        Membre aBloquer = newMembre(DESTINATAIRE_ID, "dest@test.com", "Beta", "Koné");
        when(membreRepository.findById(DESTINATAIRE_ID)).thenReturn(aBloquer);
        when(memberBlockRepository.estBloque(MEMBRE_ID, DESTINATAIRE_ID, ORG_ID)).thenReturn(true);

        BloquerMembreRequest req = BloquerMembreRequest.builder()
            .membreABloquerId(DESTINATAIRE_ID)
            .organisationId(ORG_ID)
            .build();

        assertThatThrownBy(() -> messagingService.bloquerMembre(req))
            .isInstanceOf(BadRequestException.class);
    }

    // ── debloquerMembre ───────────────────────────────────────────────────────

    @Test
    @DisplayName("debloquerMembre — blocage non trouvé → NotFoundException")
    void debloquerMembre_notFound() {
        when(memberBlockRepository.findBlocage(MEMBRE_ID, DESTINATAIRE_ID, ORG_ID))
            .thenReturn(Optional.empty());

        assertThatThrownBy(() -> messagingService.debloquerMembre(DESTINATAIRE_ID, ORG_ID))
            .isInstanceOf(NotFoundException.class);
    }

    // ── getPolitique ──────────────────────────────────────────────────────────

    @Test
    @DisplayName("getPolitique — politique existante → retourne la politique")
    void getPolitique_existing() {
        Organisation org = newOrg();
        ContactPolicy policy = ContactPolicy.builder()
            .organisation(org)
            .typePolitique(TypePolitiqueCommunication.BUREAU_SEULEMENT)
            .autoriserMembreVersMembre(false)
            .autoriserMembreVersRole(true)
            .autoriserNotesVocales(true)
            .build();
        policy.setId(UUID.randomUUID());

        when(contactPolicyRepository.findByOrganisationId(ORG_ID)).thenReturn(Optional.of(policy));

        ContactPolicyResponse result = messagingService.getPolitique(ORG_ID);

        assertThat(result.getTypePolitique()).isEqualTo("BUREAU_SEULEMENT");
        assertThat(result.isAutoriserMembreVersMembre()).isFalse();
    }

    // ── mettreAJourPolitique ──────────────────────────────────────────────────

    @Test
    @DisplayName("mettreAJourPolitique — change typePolitique")
    void mettreAJourPolitique_changeType() {
        Organisation org = newOrg();
        ContactPolicy policy = ContactPolicy.builder()
            .organisation(org)
            .typePolitique(TypePolitiqueCommunication.OUVERT)
            .autoriserMembreVersMembre(true)
            .autoriserMembreVersRole(true)
            .autoriserNotesVocales(true)
            .build();
        policy.setId(UUID.randomUUID());

        when(contactPolicyRepository.findByOrganisationId(ORG_ID)).thenReturn(Optional.of(policy));

        MettreAJourPolitiqueRequest req = MettreAJourPolitiqueRequest.builder()
            .typePolitique("BUREAU_SEULEMENT")
            .autoriserMembreVersMembre(false)
            .build();

        ContactPolicyResponse result = messagingService.mettreAJourPolitique(ORG_ID, req);

        assertThat(result.getTypePolitique()).isEqualTo("BUREAU_SEULEMENT");
        assertThat(result.isAutoriserMembreVersMembre()).isFalse();
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    private Membre newMembre(UUID id, String email, String prenom, String nom) {
        Membre m = new Membre();
        m.setId(id);
        m.setNumeroMembre("M-" + id.toString().substring(0, 4));
        m.setPrenom(prenom);
        m.setNom(nom);
        m.setEmail(email);
        m.setDateNaissance(LocalDate.now());
        return m;
    }

    private Organisation newOrg() {
        Organisation org = new Organisation();
        org.setId(ORG_ID);
        org.setNom("Tontine Test");
        return org;
    }

    private Conversation buildConversation(TypeConversation type) {
        Conversation conv = new Conversation();
        conv.setId(CONV_ID);
        conv.setOrganisation(newOrg());
        conv.setTypeConversation(type);
        conv.setStatut(StatutConversation.ACTIVE);
        conv.setNombreMessages(0);
        return conv;
    }

    private Message buildMessage(Conversation conv) {
        Membre expediteur = newMembre(MEMBRE_ID, "membre@test.com", "Alpha", "Diallo");
        Message msg = new Message();
        msg.setId(MESSAGE_ID);
        msg.setConversation(conv);
        msg.setExpediteur(expediteur);
        msg.setTypeMessage(TypeContenu.TEXTE);
        msg.setContenu("Bonjour !");
        msg.setDateCreation(LocalDateTime.now());
        return msg;
    }

    @SuppressWarnings("unchecked")
    private <T> io.quarkus.hibernate.orm.panache.PanacheQuery<T> mockSingleResultQuery(T entity) {
        io.quarkus.hibernate.orm.panache.PanacheQuery<T> query =
            org.mockito.Mockito.mock(io.quarkus.hibernate.orm.panache.PanacheQuery.class);
        when(query.firstResult()).thenReturn(entity);
        return query;
    }
}
