package dev.lions.unionflow.server.entity;

import dev.lions.unionflow.server.api.enums.membre.StatutMembre;
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import dev.lions.unionflow.server.api.enums.wave.StatutWebhook;
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Tests de couverture pour les branches manquées dans les entités.
 * Cible les branches des @PrePersist/@PreUpdate et des méthodes métier
 * non couvertes par les tests unitaires existants.
 */
@DisplayName("EntityCoverageTest — branches @PrePersist et méthodes métier")
class EntityCoverageTest {

    // ─── Membre ──────────────────────────────────────────────────────────────

    @Nested
    @DisplayName("Membre — branches manquées")
    class MembreCoverage {

        @Test
        @DisplayName("onCreate() avec statutCompte=null l'initialise à EN_ATTENTE_VALIDATION")
        void onCreate_nullStatutCompte_setsDefault() {
            // On ne peut pas appeler directement @PrePersist hors container JPA,
            // mais on peut tester la branche via reflection ou en vérifiant la valeur Builder.Default.
            // Alternative : vérifier que le Builder.Default couvre le null au niveau entité.
            Membre m = new Membre();
            m.setNumeroMembre("X");
            m.setPrenom("A");
            m.setNom("B");
            m.setEmail("a@test.com");
            m.setDateNaissance(LocalDate.now());
            // La valeur par défaut via Builder.Default est "EN_ATTENTE_VALIDATION"
            // mais l'instanciation via new Membre() ne déclenche pas le @Builder.Default.
            // On couvre la branche en appelant le @PrePersist directement via under-test:
            m.setStatutCompte(null);
            // Invoke the protected onCreate via direct call (simulated)
            m.onCreate();
            assertThat(m.getStatutCompte()).isEqualTo("EN_ATTENTE_VALIDATION");
        }

        @Test
        @DisplayName("getAge() avec dateNaissance=null retourne 0")
        void getAge_nullDateNaissance_returnsZero() {
            Membre m = new Membre();
            m.setDateNaissance(null);
            assertThat(m.getAge()).isEqualTo(0);
        }

        @Test
        @DisplayName("isMajeur() avec dateNaissance=null retourne false")
        void isMajeur_nullDateNaissance_returnsFalse() {
            Membre m = new Membre();
            m.setDateNaissance(null);
            assertThat(m.isMajeur()).isFalse();
        }
    }

    // ─── MembreOrganisation ───────────────────────────────────────────────────

    @Nested
    @DisplayName("MembreOrganisation — branches manquées")
    class MembreOrganisationCoverage {

        @Test
        @DisplayName("onCreate() avec statutMembre=null l'initialise à EN_ATTENTE_VALIDATION")
        void onCreate_nullStatutMembre_setsDefault() {
            MembreOrganisation mo = new MembreOrganisation();
            mo.setStatutMembre(null);
            mo.onCreate();
            assertThat(mo.getStatutMembre()).isEqualTo(StatutMembre.EN_ATTENTE_VALIDATION);
        }
    }

    // ─── ModuleOrganisationActif ──────────────────────────────────────────────

    @Nested
    @DisplayName("ModuleOrganisationActif — isActif() non couvert")
    class ModuleOrganisationActifCoverage {

        @Test
        @DisplayName("isActif() retourne true quand actif=true")
        void isActif_true() {
            ModuleOrganisationActif m = new ModuleOrganisationActif();
            m.setActif(true);
            m.setModuleCode("TEST");
            assertThat(m.isActif()).isTrue();
        }

        @Test
        @DisplayName("isActif() retourne false quand actif=false")
        void isActif_false() {
            ModuleOrganisationActif m = new ModuleOrganisationActif();
            m.setActif(false);
            m.setModuleCode("TEST");
            assertThat(m.isActif()).isFalse();
        }

        @Test
        @DisplayName("isActif() retourne false quand actif=null")
        void isActif_null() {
            ModuleOrganisationActif m = new ModuleOrganisationActif();
            m.setActif(null);
            assertThat(m.isActif()).isFalse();
        }
    }

    // ─── InscriptionEvenement — preUpdate ─────────────────────────────────────

    @Nested
    @DisplayName("InscriptionEvenement — preUpdate() non couvert")
    class InscriptionEvenementCoverage {

        @Test
        @DisplayName("preUpdate() appelle super.onUpdate() sans exception")
        void preUpdate_doesNotThrow() {
            InscriptionEvenement ie = new InscriptionEvenement();
            ie.setStatut("CONFIRMEE");
            ie.preUpdate();
            // La date de modification est setDateModification — on vérifie juste que ça ne plante pas
            assertThat(ie.getDateModification()).isNotNull();
        }
    }

    // ─── Conversation v4 ────────────────────────────────────────────────────

    @Nested
    @DisplayName("Conversation v4 — méthodes métier")
    class ConversationCoverage {

        @Test
        @DisplayName("Conversation getters/setters v4")
        void gettersSetters() {
            Conversation c = new Conversation();
            c.setTypeConversation(TypeConversation.DIRECTE);
            c.setRoleCible("TRESORIER");
            c.setTitre("Canal Trésorier");
            c.setStatut(StatutConversation.ACTIVE);
            c.setDernierMessageAt(LocalDateTime.now());
            c.setNombreMessages(3);

            assertThat(c.getTypeConversation()).isEqualTo(TypeConversation.DIRECTE);
            assertThat(c.getRoleCible()).isEqualTo("TRESORIER");
            assertThat(c.getTitre()).isEqualTo("Canal Trésorier");
            assertThat(c.getStatut()).isEqualTo(StatutConversation.ACTIVE);
            assertThat(c.getNombreMessages()).isEqualTo(3);
        }

        @Test
        @DisplayName("estActive() — ACTIVE → true, ARCHIVEE → false")
        void estActive() {
            Conversation active = new Conversation();
            active.setStatut(StatutConversation.ACTIVE);
            assertThat(active.estActive()).isTrue();

            Conversation archivee = new Conversation();
            archivee.setStatut(StatutConversation.ARCHIVEE);
            assertThat(archivee.estActive()).isFalse();
        }

        @Test
        @DisplayName("archiver() — passe à ARCHIVEE")
        void archiver() {
            Conversation c = new Conversation();
            c.setStatut(StatutConversation.ACTIVE);
            c.archiver();
            assertThat(c.getStatut()).isEqualTo(StatutConversation.ARCHIVEE);
        }

        @Test
        @DisplayName("enregistrerNouveauMessage() — incrémente compteur et met à jour horodatage")
        void enregistrerNouveauMessage() {
            Conversation c = new Conversation();
            c.setNombreMessages(2);
            LocalDateTime avant = LocalDateTime.now().minusSeconds(1);

            c.enregistrerNouveauMessage();

            assertThat(c.getNombreMessages()).isEqualTo(3);
            assertThat(c.getDernierMessageAt()).isNotNull().isAfter(avant);
        }

        @Test
        @DisplayName("enregistrerNouveauMessage() — nombreMessages null → passe à 1")
        void enregistrerNouveauMessage_nullCount() {
            Conversation c = new Conversation();
            c.setNombreMessages(null);
            c.enregistrerNouveauMessage();
            assertThat(c.getNombreMessages()).isEqualTo(1);
        }

        @Test
        @DisplayName("onCreate() — statut null → initialisé à ACTIVE")
        void onCreate_statut() {
            Conversation c = new Conversation();
            c.setStatut(null);
            c.setTypeConversation(TypeConversation.ROLE_CANAL);
            c.setId(java.util.UUID.randomUUID());
            c.setDateCreation(LocalDateTime.now());
            c.setActif(true);
            c.setVersion(0L);
            c.onCreate();
            assertThat(c.getStatut()).isEqualTo(StatutConversation.ACTIVE);
        }
    }

    // ─── EcritureComptable — onUpdate ────────────────────────────────────────

    @Nested
    @DisplayName("EcritureComptable — onUpdate() non couvert")
    class EcritureComptableCoverage {

        @Test
        @DisplayName("onUpdate() appelle calculerTotaux")
        void onUpdate_calculerTotaux() {
            EcritureComptable e = new EcritureComptable();
            e.setNumeroPiece("ECR-UPD-001");
            e.setDateEcriture(java.time.LocalDate.now());
            e.setLibelle("Test update");
            e.setMontantDebit(java.math.BigDecimal.ZERO);
            e.setMontantCredit(java.math.BigDecimal.ZERO);
            // onUpdate calls calculerTotaux; with empty lignes, totals reset to ZERO
            e.onUpdate();
            assertThat(e.getMontantDebit()).isEqualByComparingTo(java.math.BigDecimal.ZERO);
            assertThat(e.getMontantCredit()).isEqualByComparingTo(java.math.BigDecimal.ZERO);
        }
    }

    // ─── ValidationEtapeDemande — onCreate ───────────────────────────────────

    @Nested
    @DisplayName("ValidationEtapeDemande — onCreate() non couvert")
    class ValidationEtapeDemandeCoverage {

        @Test
        @DisplayName("onCreate() avec statut=null initialise à EN_ATTENTE")
        void onCreate_nullStatut_setsDefault() {
            dev.lions.unionflow.server.api.enums.solidarite.StatutValidationEtape statut =
                    dev.lions.unionflow.server.api.enums.solidarite.StatutValidationEtape.EN_ATTENTE;
            ValidationEtapeDemande ved = new ValidationEtapeDemande();
            ved.setStatut(null);
            ved.onCreate();
            assertThat(ved.getStatut()).isEqualTo(statut);
        }

        @Test
        @DisplayName("onCreate() avec statut déjà défini le préserve")
        void onCreate_existingStatut_preserves() {
            dev.lions.unionflow.server.api.enums.solidarite.StatutValidationEtape statut =
                    dev.lions.unionflow.server.api.enums.solidarite.StatutValidationEtape.APPROUVEE;
            ValidationEtapeDemande ved = new ValidationEtapeDemande();
            ved.setStatut(statut);
            ved.onCreate();
            assertThat(ved.getStatut()).isEqualTo(statut);
        }
    }

    // ─── SystemAlert — branches @PrePersist ──────────────────────────────────

    @Nested
    @DisplayName("SystemAlert — branches @PrePersist non couvertes")
    class SystemAlertCoverage {

        @Test
        @DisplayName("onCreate() avec timestamp=null l'initialise")
        void onCreate_nullTimestamp_setsNow() {
            SystemAlert a = new SystemAlert();
            a.setLevel("WARNING");
            a.setTitle("Test");
            a.setMessage("Msg");
            a.setTimestamp(null);
            a.setAcknowledged(null);
            a.onCreate();
            assertThat(a.getTimestamp()).isNotNull();
            assertThat(a.getAcknowledged()).isFalse();
        }

        @Test
        @DisplayName("onCreate() avec timestamp et acknowledged déjà définis ne les écrase pas")
        void onCreate_withExistingValues_preserves() {
            LocalDateTime ts = LocalDateTime.of(2026, 1, 1, 0, 0);
            SystemAlert a = new SystemAlert();
            a.setTimestamp(ts);
            a.setAcknowledged(true);
            a.onCreate();
            assertThat(a.getTimestamp()).isEqualTo(ts);
            assertThat(a.getAcknowledged()).isTrue();
        }
    }

    // ─── SystemLog — branche @PrePersist ─────────────────────────────────────

    @Nested
    @DisplayName("SystemLog — branche @PrePersist non couverte")
    class SystemLogCoverage {

        @Test
        @DisplayName("onCreate() avec timestamp=null l'initialise")
        void onCreate_nullTimestamp_setsNow() {
            SystemLog sl = new SystemLog();
            sl.setLevel("ERROR");
            sl.setSource("TEST");
            sl.setMessage("Message test");
            sl.setTimestamp(null);
            sl.onCreate();
            assertThat(sl.getTimestamp()).isNotNull();
        }

        @Test
        @DisplayName("Getters/setters de SystemLog")
        void gettersSetters() {
            SystemLog sl = new SystemLog();
            sl.setLevel("INFO");
            sl.setSource("Database");
            sl.setMessage("DB query");
            sl.setDetails("stack trace here");
            sl.setTimestamp(LocalDateTime.now());
            sl.setUserId("user-123");
            sl.setSessionId("sess-abc");
            sl.setEndpoint("/api/test");
            sl.setHttpStatusCode(200);

            assertThat(sl.getLevel()).isEqualTo("INFO");
            assertThat(sl.getSource()).isEqualTo("Database");
            assertThat(sl.getEndpoint()).isEqualTo("/api/test");
            assertThat(sl.getHttpStatusCode()).isEqualTo(200);
        }
    }

    // ─── WebhookWave — branches @PrePersist ──────────────────────────────────

    @Nested
    @DisplayName("WebhookWave — branches @PrePersist non couvertes")
    class WebhookWaveCoverage {

        @Test
        @DisplayName("onCreate() avec statutTraitement=null l'initialise à EN_ATTENTE")
        void onCreate_nullStatut_setsDefault() {
            WebhookWave w = new WebhookWave();
            w.setWaveEventId("evt-" + UUID.randomUUID());
            w.setStatutTraitement(null);
            w.setNombreTentatives(null);
            w.setDateReception(null);
            w.onCreate();
            assertThat(w.getStatutTraitement()).isEqualTo(StatutWebhook.EN_ATTENTE.name());
            assertThat(w.getNombreTentatives()).isEqualTo(0);
            assertThat(w.getDateReception()).isNotNull();
        }

        @Test
        @DisplayName("onCreate() ne surécrit pas des valeurs déjà définies")
        void onCreate_withExistingValues_preserves() {
            WebhookWave w = new WebhookWave();
            w.setWaveEventId("evt-" + UUID.randomUUID());
            w.setStatutTraitement(StatutWebhook.TRAITE.name());
            w.setNombreTentatives(3);
            LocalDateTime reception = LocalDateTime.of(2026, 1, 1, 12, 0);
            w.setDateReception(reception);
            w.onCreate();
            assertThat(w.getStatutTraitement()).isEqualTo(StatutWebhook.TRAITE.name());
            assertThat(w.getNombreTentatives()).isEqualTo(3);
            assertThat(w.getDateReception()).isEqualTo(reception);
        }
    }

    // ─── Paiement — branches @PrePersist ─────────────────────────────────────

    @Nested
    @DisplayName("Paiement — branches @PrePersist non couvertes")
    class PaiementCoverage {

        private Membre newMembre() {
            Membre m = new Membre();
            m.setId(UUID.randomUUID());
            m.setNumeroMembre("MX");
            m.setPrenom("X");
            m.setNom("Y");
            m.setEmail("xy@test.com");
            m.setDateNaissance(LocalDate.now());
            return m;
        }

        @Test
        @DisplayName("onCreate() avec numeroReference=null génère un numéro")
        void onCreate_nullNumeroReference_generates() {
            Paiement p = new Paiement();
            p.setNumeroReference(null);
            p.setStatutPaiement(null);
            p.setMontant(BigDecimal.ONE);
            p.setCodeDevise("XOF");
            p.setMethodePaiement("WAVE");
            p.setMembre(newMembre());
            p.onCreate();
            assertThat(p.getNumeroReference()).startsWith("PAY-");
            assertThat(p.getStatutPaiement()).isEqualTo("EN_ATTENTE");
            assertThat(p.getDatePaiement()).isNotNull();
        }

        @Test
        @DisplayName("onCreate() avec numeroReference vide génère un numéro")
        void onCreate_emptyNumeroReference_generates() {
            Paiement p = new Paiement();
            p.setNumeroReference("");
            p.setStatutPaiement("VALIDE");
            p.setMontant(BigDecimal.ONE);
            p.setCodeDevise("XOF");
            p.setMethodePaiement("WAVE");
            p.setMembre(newMembre());
            p.setDatePaiement(LocalDateTime.now()); // already set
            p.onCreate();
            assertThat(p.getNumeroReference()).startsWith("PAY-");
        }

        @Test
        @DisplayName("peutEtreModifie() retourne false pour ANNULE")
        void peutEtreModifie_annule_returnsFalse() {
            Paiement p = new Paiement();
            p.setNumeroReference("X");
            p.setMontant(BigDecimal.ONE);
            p.setCodeDevise("XOF");
            p.setMethodePaiement("WAVE");
            p.setMembre(newMembre());
            p.setStatutPaiement("ANNULE");
            assertThat(p.peutEtreModifie()).isFalse();
        }
    }

    // ─── DemandeAide — branches @PrePersist ──────────────────────────────────

    @Nested
    @DisplayName("DemandeAide — branches @PrePersist non couvertes")
    class DemandeAideCoverage {

        @Test
        @DisplayName("onCreate() avec dateDemande/statut/urgence=null initialise les defaults")
        void onCreate_nullFields_setsDefaults() {
            DemandeAide d = new DemandeAide();
            d.setDateDemande(null);
            d.setStatut(null);
            d.setUrgence(null);
            d.onCreate();
            assertThat(d.getDateDemande()).isNotNull();
            assertThat(d.getStatut()).isEqualTo(StatutAide.EN_ATTENTE);
            assertThat(d.getUrgence()).isFalse();
        }
    }

    // ─── LigneEcriture — branches @PrePersist + getMontant ───────────────────

    @Nested
    @DisplayName("LigneEcriture — branches non couvertes")
    class LigneEcritureCoverage {

        @Test
        @DisplayName("onCreate() avec montantDebit/montantCredit=null les initialise à ZERO")
        void onCreate_nullMontants_setsZero() {
            LigneEcriture le = new LigneEcriture();
            le.setMontantDebit(null);
            le.setMontantCredit(null);
            le.onCreate();
            assertThat(le.getMontantDebit()).isEqualByComparingTo(BigDecimal.ZERO);
            assertThat(le.getMontantCredit()).isEqualByComparingTo(BigDecimal.ZERO);
        }

        @Test
        @DisplayName("getMontant() retourne ZERO quand les deux montants sont null/zero")
        void getMontant_noDebitNoCredit_returnsZero() {
            LigneEcriture le = new LigneEcriture();
            le.setMontantDebit(null);
            le.setMontantCredit(null);
            assertThat(le.getMontant()).isEqualByComparingTo(BigDecimal.ZERO);
        }
    }

    // ─── MembreRole — branches isActif ────────────────────────────────────────

    @Nested
    @DisplayName("MembreRole — branches isActif() et @PrePersist non couverts")
    class MembreRoleCoverage {

        private MembreRole newMembreRole() {
            MembreRole mr = new MembreRole();
            mr.setActif(true);
            return mr;
        }

        @Test
        @DisplayName("isActif() retourne false si actif=false")
        void isActif_inactif_returnsFalse() {
            MembreRole mr = newMembreRole();
            mr.setActif(false);
            assertThat(mr.isActif()).isFalse();
        }

        @Test
        @DisplayName("isActif() retourne false si dateFin est dépassée")
        void isActif_dateFinPassed_returnsFalse() {
            MembreRole mr = newMembreRole();
            mr.setDateDebut(LocalDate.now().minusDays(10));
            mr.setDateFin(LocalDate.now().minusDays(1));
            assertThat(mr.isActif()).isFalse();
        }

        @Test
        @DisplayName("onCreate() avec dateDebut=null l'initialise à aujourd'hui")
        void onCreate_nullDateDebut_setsToday() {
            MembreRole mr = new MembreRole();
            mr.setDateDebut(null);
            mr.onCreate();
            assertThat(mr.getDateDebut()).isEqualTo(LocalDate.now());
        }
    }

    // ─── Permission — branche @PrePersist ────────────────────────────────────

    @Nested
    @DisplayName("Permission — branche @PrePersist non couverte")
    class PermissionCoverage {

        @Test
        @DisplayName("onCreate() avec code=null et module/ressource/action définis génère le code")
        void onCreate_nullCode_withModuleRessourceAction_generatesCode() {
            Permission p = new Permission();
            p.setCode(null);
            p.setModule("FINANCE");
            p.setRessource("COTISATION");
            p.setAction("CREATE");
            p.onCreate();
            assertThat(p.getCode()).isEqualTo("FINANCE > COTISATION > CREATE");
        }

        @Test
        @DisplayName("onCreate() avec code=null et module=null ne génère rien")
        void onCreate_nullCode_nullModule_noGeneration() {
            Permission p = new Permission();
            p.setCode(null);
            p.setModule(null);
            p.setRessource(null);
            p.setAction(null);
            p.onCreate();
            assertThat(p.getCode()).isNull();
        }
    }

    // ─── Role — branches @PrePersist ─────────────────────────────────────────

    @Nested
    @DisplayName("Role — branches @PrePersist non couvertes")
    class RoleCoverage {

        @Test
        @DisplayName("onCreate() avec typeRole/niveauHierarchique=null initialise les defaults")
        void onCreate_nullFields_setsDefaults() {
            Role r = new Role();
            r.setTypeRole(null);
            r.setNiveauHierarchique(null);
            r.onCreate();
            assertThat(r.getTypeRole()).isNotNull();
            assertThat(r.getNiveauHierarchique()).isEqualTo(100);
        }
    }

    // ─── Document — branches @PrePersist ─────────────────────────────────────

    @Nested
    @DisplayName("Document — branches @PrePersist non couvertes")
    class DocumentCoverage {

        @Test
        @DisplayName("onCreate() avec nombreTelechargements/typeDocument=null initialise les defaults")
        void onCreate_nullFields_setsDefaults() {
            Document d = new Document();
            d.setNombreTelechargements(null);
            d.setTypeDocument(null);
            d.onCreate();
            assertThat(d.getNombreTelechargements()).isEqualTo(0);
            assertThat(d.getTypeDocument()).isNotNull();
        }
    }

    // ─── PieceJointe — branche @PrePersist ───────────────────────────────────

    @Nested
    @DisplayName("PieceJointe — branche @PrePersist non couverte")
    class PieceJointeCoverage {

        @Test
        @DisplayName("onCreate() avec ordre=null l'initialise à 1")
        void onCreate_nullOrdre_setsOne() {
            PieceJointe pj = new PieceJointe();
            pj.setOrdre(null);
            pj.onCreate();
            assertThat(pj.getOrdre()).isEqualTo(1);
        }
    }

    // ─── Adresse — branche @PrePersist ───────────────────────────────────────

    @Nested
    @DisplayName("Adresse — branche @PrePersist non couverte")
    class AdresseCoverage {

        @Test
        @DisplayName("onCreate() avec principale=null l'initialise à false")
        void onCreate_nullPrincipale_setsFalse() {
            Adresse a = new Adresse();
            a.setPrincipale(null);
            a.onCreate();
            assertThat(a.getPrincipale()).isFalse();
        }
    }

    // ─── JournalComptable — branche @PrePersist ──────────────────────────────

    @Nested
    @DisplayName("JournalComptable — branche @PrePersist non couverte")
    class JournalComptableCoverage {

        @Test
        @DisplayName("onCreate() avec statut=null initialise à OUVERT")
        void onCreate_nullStatut_setsOuvert() {
            JournalComptable jc = new JournalComptable();
            jc.setStatut(null);
            jc.onCreate();
            assertThat(jc.getStatut()).isEqualTo("OUVERT");
        }

        @Test
        @DisplayName("onCreate() avec statut vide initialise à OUVERT")
        void onCreate_emptyStatut_setsOuvert() {
            JournalComptable jc = new JournalComptable();
            jc.setStatut("");
            jc.onCreate();
            assertThat(jc.getStatut()).isEqualTo("OUVERT");
        }
    }

    // ─── SuggestionVote — branches @PrePersist ───────────────────────────────

    @Nested
    @DisplayName("SuggestionVote — branches @PrePersist non couvertes")
    class SuggestionVoteCoverage {

        @Test
        @DisplayName("onPrePersist() avec dateVote=null l'initialise")
        void onPrePersist_nullDateVote_setsNow() {
            SuggestionVote sv = new SuggestionVote();
            sv.setSuggestionId(UUID.randomUUID());
            sv.setUtilisateurId(UUID.randomUUID());
            sv.setDateVote(null);
            sv.setDateCreation(null);
            sv.onPrePersist();
            assertThat(sv.getDateVote()).isNotNull();
            assertThat(sv.getDateCreation()).isNotNull();
        }
    }
}
