package dev.lions.unionflow.server.entity;

import dev.lions.unionflow.server.api.enums.wave.StatutCompteWave;
import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave;
import dev.lions.unionflow.server.api.enums.wave.TypeTransactionWave;
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.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

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

@DisplayName("TransactionWave — couverture complète")
class TransactionWaveTest {

    // ─── Utilitaire ───────────────────────────────────────────────────────────

    private static CompteWave newCompteWave() {
        CompteWave c = new CompteWave();
        c.setId(UUID.randomUUID());
        c.setNumeroTelephone("+22507000001");
        c.setStatutCompte(StatutCompteWave.VERIFIE);
        return c;
    }

    private static TransactionWave baseTransaction() {
        TransactionWave t = new TransactionWave();
        t.setWaveTransactionId("wave-123");
        t.setTypeTransaction(TypeTransactionWave.DEPOT);
        t.setStatutTransaction(StatutTransactionWave.REUSSIE);
        t.setMontant(new BigDecimal("5000.00"));
        t.setCodeDevise("XOF");
        t.setCompteWave(newCompteWave());
        return t;
    }

    // ─── Getters / Setters ────────────────────────────────────────────────────

    @Nested
    @DisplayName("Getters et setters")
    class GettersSetters {

        @Test
        @DisplayName("Champs de base déjà couverts")
        void champsDeBase() {
            TransactionWave t = baseTransaction();

            assertThat(t.getWaveTransactionId()).isEqualTo("wave-123");
            assertThat(t.getTypeTransaction()).isEqualTo(TypeTransactionWave.DEPOT);
            assertThat(t.getStatutTransaction()).isEqualTo(StatutTransactionWave.REUSSIE);
            assertThat(t.getMontant()).isEqualByComparingTo("5000.00");
            assertThat(t.getCodeDevise()).isEqualTo("XOF");
            assertThat(t.getCompteWave()).isNotNull();
        }

        @Test
        @DisplayName("waveRequestId et waveReference")
        void waveRequestIdEtReference() {
            TransactionWave t = baseTransaction();
            t.setWaveRequestId("req-abc-456");
            t.setWaveReference("ref-xyz-789");

            assertThat(t.getWaveRequestId()).isEqualTo("req-abc-456");
            assertThat(t.getWaveReference()).isEqualTo("ref-xyz-789");
        }

        @Test
        @DisplayName("frais, montantNet")
        void fraisEtMontantNet() {
            TransactionWave t = baseTransaction();
            t.setFrais(new BigDecimal("150.00"));
            t.setMontantNet(new BigDecimal("4850.00"));

            assertThat(t.getFrais()).isEqualByComparingTo("150.00");
            assertThat(t.getMontantNet()).isEqualByComparingTo("4850.00");
        }

        @Test
        @DisplayName("telephonePayeur et telephoneBeneficiaire")
        void telephones() {
            TransactionWave t = baseTransaction();
            t.setTelephonePayeur("+22507111111");
            t.setTelephoneBeneficiaire("+22507222222");

            assertThat(t.getTelephonePayeur()).isEqualTo("+22507111111");
            assertThat(t.getTelephoneBeneficiaire()).isEqualTo("+22507222222");
        }

        @Test
        @DisplayName("metadonnees et reponseWaveApi")
        void metadonneesEtReponse() {
            TransactionWave t = baseTransaction();
            t.setMetadonnees("{\"key\":\"val\"}");
            t.setReponseWaveApi("{\"status\":\"success\"}");

            assertThat(t.getMetadonnees()).isEqualTo("{\"key\":\"val\"}");
            assertThat(t.getReponseWaveApi()).isEqualTo("{\"status\":\"success\"}");
        }

        @Test
        @DisplayName("nombreTentatives, dateDerniereTentative, messageErreur")
        void tentativesEtErreur() {
            LocalDateTime now = LocalDateTime.now();
            TransactionWave t = baseTransaction();
            t.setNombreTentatives(3);
            t.setDateDerniereTentative(now);
            t.setMessageErreur("Délai dépassé");

            assertThat(t.getNombreTentatives()).isEqualTo(3);
            assertThat(t.getDateDerniereTentative()).isEqualTo(now);
            assertThat(t.getMessageErreur()).isEqualTo("Délai dépassé");
        }

        @Test
        @DisplayName("webhooks — liste modifiable")
        void webhooks() {
            TransactionWave t = baseTransaction();
            List<WebhookWave> webhooks = new ArrayList<>();
            WebhookWave wh = new WebhookWave();
            wh.setWaveEventId("evt-001");
            webhooks.add(wh);
            t.setWebhooks(webhooks);

            assertThat(t.getWebhooks()).hasSize(1);
            assertThat(t.getWebhooks().get(0).getWaveEventId()).isEqualTo("evt-001");
        }

        @Test
        @DisplayName("Champs hérités de BaseEntity (id, dateCreation, creePar, modifiePar, version, actif)")
        void champsBaseEntity() {
            UUID id = UUID.randomUUID();
            LocalDateTime now = LocalDateTime.now();
            TransactionWave t = baseTransaction();
            t.setId(id);
            t.setDateCreation(now);
            t.setDateModification(now);
            t.setCreePar("admin@test.com");
            t.setModifiePar("user@test.com");
            t.setVersion(1L);
            t.setActif(true);

            assertThat(t.getId()).isEqualTo(id);
            assertThat(t.getDateCreation()).isEqualTo(now);
            assertThat(t.getDateModification()).isEqualTo(now);
            assertThat(t.getCreePar()).isEqualTo("admin@test.com");
            assertThat(t.getModifiePar()).isEqualTo("user@test.com");
            assertThat(t.getVersion()).isEqualTo(1L);
            assertThat(t.getActif()).isTrue();
        }
    }

    // ─── Builder ──────────────────────────────────────────────────────────────

    @Nested
    @DisplayName("Builder Lombok")
    class BuilderTest {

        @Test
        @DisplayName("Builder avec tous les champs obligatoires")
        void builderChampsObligatoires() {
            CompteWave compte = newCompteWave();
            TransactionWave t = TransactionWave.builder()
                    .waveTransactionId("builder-wave-001")
                    .typeTransaction(TypeTransactionWave.PAIEMENT)
                    .statutTransaction(StatutTransactionWave.EN_ATTENTE)
                    .montant(new BigDecimal("10000.00"))
                    .codeDevise("XOF")
                    .compteWave(compte)
                    .build();

            assertThat(t.getWaveTransactionId()).isEqualTo("builder-wave-001");
            assertThat(t.getTypeTransaction()).isEqualTo(TypeTransactionWave.PAIEMENT);
            assertThat(t.getStatutTransaction()).isEqualTo(StatutTransactionWave.EN_ATTENTE);
            assertThat(t.getMontant()).isEqualByComparingTo("10000.00");
            assertThat(t.getCodeDevise()).isEqualTo("XOF");
            assertThat(t.getCompteWave()).isEqualTo(compte);
        }

        @Test
        @DisplayName("Builder : valeurs @Builder.Default — statutTransaction=INITIALISE, nombreTentatives=0, webhooks vide")
        void builderDefaults() {
            CompteWave compte = newCompteWave();
            TransactionWave t = TransactionWave.builder()
                    .waveTransactionId("builder-wave-defaults")
                    .typeTransaction(TypeTransactionWave.RETRAIT)
                    .montant(new BigDecimal("500.00"))
                    .codeDevise("XOF")
                    .compteWave(compte)
                    .build();

            assertThat(t.getStatutTransaction()).isEqualTo(StatutTransactionWave.INITIALISE);
            assertThat(t.getNombreTentatives()).isEqualTo(0);
            assertThat(t.getWebhooks()).isNotNull().isEmpty();
        }

        @Test
        @DisplayName("Builder avec tous les champs facultatifs")
        void builderChampsOptionnels() {
            LocalDateTime now = LocalDateTime.now();
            CompteWave compte = newCompteWave();
            TransactionWave t = TransactionWave.builder()
                    .waveTransactionId("builder-wave-full")
                    .waveRequestId("req-full-001")
                    .waveReference("ref-full-001")
                    .typeTransaction(TypeTransactionWave.TRANSFERT)
                    .statutTransaction(StatutTransactionWave.REUSSIE)
                    .montant(new BigDecimal("20000.00"))
                    .frais(new BigDecimal("200.00"))
                    .montantNet(new BigDecimal("19800.00"))
                    .codeDevise("XOF")
                    .telephonePayeur("+22507001001")
                    .telephoneBeneficiaire("+22507002002")
                    .metadonnees("{}")
                    .reponseWaveApi("{\"code\":\"200\"}")
                    .nombreTentatives(1)
                    .dateDerniereTentative(now)
                    .messageErreur(null)
                    .compteWave(compte)
                    .build();

            assertThat(t.getWaveRequestId()).isEqualTo("req-full-001");
            assertThat(t.getWaveReference()).isEqualTo("ref-full-001");
            assertThat(t.getFrais()).isEqualByComparingTo("200.00");
            assertThat(t.getMontantNet()).isEqualByComparingTo("19800.00");
            assertThat(t.getTelephonePayeur()).isEqualTo("+22507001001");
            assertThat(t.getTelephoneBeneficiaire()).isEqualTo("+22507002002");
            assertThat(t.getMetadonnees()).isEqualTo("{}");
            assertThat(t.getReponseWaveApi()).isEqualTo("{\"code\":\"200\"}");
            assertThat(t.getNombreTentatives()).isEqualTo(1);
            assertThat(t.getDateDerniereTentative()).isEqualTo(now);
            assertThat(t.getMessageErreur()).isNull();
        }
    }

    // ─── Méthodes métier ──────────────────────────────────────────────────────

    @Nested
    @DisplayName("Méthodes métier")
    class MethodesMetier {

        @Test
        @DisplayName("isReussie() retourne true si statut=REUSSIE")
        void isReussie_statutReussie_retourneTrue() {
            TransactionWave t = baseTransaction();
            t.setStatutTransaction(StatutTransactionWave.REUSSIE);
            assertThat(t.isReussie()).isTrue();
        }

        @Test
        @DisplayName("isReussie() retourne false si statut=ECHOUE")
        void isReussie_statutEchoue_retourneFalse() {
            TransactionWave t = baseTransaction();
            t.setStatutTransaction(StatutTransactionWave.ECHOUE);
            assertThat(t.isReussie()).isFalse();
        }

        @Test
        @DisplayName("isReussie() retourne false si statut=INITIALISE")
        void isReussie_statutInitialise_retourneFalse() {
            TransactionWave t = baseTransaction();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            assertThat(t.isReussie()).isFalse();
        }

        @Test
        @DisplayName("peutEtreRetentee() retourne true si statut=ECHOUE et tentatives<5")
        void peutEtreRetentee_echoue_tentativesFaibles_retourneTrue() {
            TransactionWave t = baseTransaction();
            t.setStatutTransaction(StatutTransactionWave.ECHOUE);
            t.setNombreTentatives(2);
            assertThat(t.peutEtreRetentee()).isTrue();
        }

        @Test
        @DisplayName("peutEtreRetentee() retourne true si statut=EXPIRED et tentatives<5")
        void peutEtreRetentee_expired_tentativesFaibles_retourneTrue() {
            TransactionWave t = baseTransaction();
            t.setStatutTransaction(StatutTransactionWave.EXPIRED);
            t.setNombreTentatives(0);
            assertThat(t.peutEtreRetentee()).isTrue();
        }

        @Test
        @DisplayName("peutEtreRetentee() retourne false si tentatives=5")
        void peutEtreRetentee_tentativesMax_retourneFalse() {
            TransactionWave t = baseTransaction();
            t.setStatutTransaction(StatutTransactionWave.ECHOUE);
            t.setNombreTentatives(5);
            assertThat(t.peutEtreRetentee()).isFalse();
        }

        @Test
        @DisplayName("peutEtreRetentee() retourne false si statut=REUSSIE")
        void peutEtreRetentee_statutReussie_retourneFalse() {
            TransactionWave t = baseTransaction();
            t.setStatutTransaction(StatutTransactionWave.REUSSIE);
            t.setNombreTentatives(1);
            assertThat(t.peutEtreRetentee()).isFalse();
        }

        @Test
        @DisplayName("peutEtreRetentee() retourne true si nombreTentatives=null")
        void peutEtreRetentee_tentativesNull_retourneTrue() {
            TransactionWave t = baseTransaction();
            t.setStatutTransaction(StatutTransactionWave.ECHOUE);
            t.setNombreTentatives(null);
            assertThat(t.peutEtreRetentee()).isTrue();
        }

        @Test
        @DisplayName("peutEtreRetentee() retourne false si statut=INITIALISE")
        void peutEtreRetentee_statutInitialise_retourneFalse() {
            TransactionWave t = baseTransaction();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setNombreTentatives(1);
            assertThat(t.peutEtreRetentee()).isFalse();
        }

        @Test
        @DisplayName("peutEtreRetentee() retourne false si statut=ANNULEE")
        void peutEtreRetentee_statutAnnulee_retourneFalse() {
            TransactionWave t = baseTransaction();
            t.setStatutTransaction(StatutTransactionWave.ANNULEE);
            t.setNombreTentatives(0);
            assertThat(t.peutEtreRetentee()).isFalse();
        }
    }

    // ─── @PrePersist onCreate ─────────────────────────────────────────────────

    @Nested
    @DisplayName("@PrePersist onCreate()")
    class OnCreate {

        @Test
        @DisplayName("statutTransaction=null est initialisé à INITIALISE")
        void statutTransactionNull_initialiseeAInitialise() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(null);
            t.setCodeDevise("XOF");
            t.setNombreTentatives(0);
            t.setMontant(new BigDecimal("1000.00"));
            t.onCreate();

            assertThat(t.getStatutTransaction()).isEqualTo(StatutTransactionWave.INITIALISE);
        }

        @Test
        @DisplayName("statutTransaction déjà défini n'est pas écrasé")
        void statutTransactionDejaDefini_nonEcrase() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.EN_COURS);
            t.setCodeDevise("XOF");
            t.setNombreTentatives(0);
            t.setMontant(new BigDecimal("1000.00"));
            t.onCreate();

            assertThat(t.getStatutTransaction()).isEqualTo(StatutTransactionWave.EN_COURS);
        }

        @Test
        @DisplayName("codeDevise=null est initialisé à XOF")
        void codeDeviseNull_initialiseeAXof() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setCodeDevise(null);
            t.setNombreTentatives(0);
            t.setMontant(new BigDecimal("1000.00"));
            t.onCreate();

            assertThat(t.getCodeDevise()).isEqualTo("XOF");
        }

        @Test
        @DisplayName("codeDevise vide ('') est initialisé à XOF")
        void codeDeviseVide_initialiseeAXof() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setCodeDevise("");
            t.setNombreTentatives(0);
            t.setMontant(new BigDecimal("1000.00"));
            t.onCreate();

            assertThat(t.getCodeDevise()).isEqualTo("XOF");
        }

        @Test
        @DisplayName("codeDevise déjà défini n'est pas écrasé")
        void codeDeviseDejaDefini_nonEcrase() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setCodeDevise("EUR");
            t.setNombreTentatives(0);
            t.setMontant(new BigDecimal("1000.00"));
            t.onCreate();

            assertThat(t.getCodeDevise()).isEqualTo("EUR");
        }

        @Test
        @DisplayName("nombreTentatives=null est initialisé à 0")
        void nombreTentativesNull_initialiseeAZero() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setCodeDevise("XOF");
            t.setNombreTentatives(null);
            t.setMontant(new BigDecimal("1000.00"));
            t.onCreate();

            assertThat(t.getNombreTentatives()).isEqualTo(0);
        }

        @Test
        @DisplayName("nombreTentatives déjà défini n'est pas écrasé")
        void nombreTentativesDejaDefini_nonEcrase() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setCodeDevise("XOF");
            t.setNombreTentatives(3);
            t.setMontant(new BigDecimal("1000.00"));
            t.onCreate();

            assertThat(t.getNombreTentatives()).isEqualTo(3);
        }

        @Test
        @DisplayName("montantNet=null est calculé quand montant et frais sont définis")
        void montantNetNull_calculeDepuisMontantEtFrais() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setCodeDevise("XOF");
            t.setNombreTentatives(0);
            t.setMontant(new BigDecimal("5000.00"));
            t.setFrais(new BigDecimal("150.00"));
            t.setMontantNet(null);
            t.onCreate();

            assertThat(t.getMontantNet()).isEqualByComparingTo("4850.00");
        }

        @Test
        @DisplayName("montantNet=null n'est pas calculé si frais=null")
        void montantNetNull_nonCalculeSiFraisNull() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setCodeDevise("XOF");
            t.setNombreTentatives(0);
            t.setMontant(new BigDecimal("5000.00"));
            t.setFrais(null);
            t.setMontantNet(null);
            t.onCreate();

            assertThat(t.getMontantNet()).isNull();
        }

        @Test
        @DisplayName("montantNet=null n'est pas calculé si montant=null")
        void montantNetNull_nonCalculeSiMontantNull() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setCodeDevise("XOF");
            t.setNombreTentatives(0);
            t.setMontant(null);
            t.setFrais(new BigDecimal("150.00"));
            t.setMontantNet(null);
            t.onCreate();

            assertThat(t.getMontantNet()).isNull();
        }

        @Test
        @DisplayName("montantNet déjà défini n'est pas recalculé")
        void montantNetDejaDefini_nonRecalcule() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setCodeDevise("XOF");
            t.setNombreTentatives(0);
            t.setMontant(new BigDecimal("5000.00"));
            t.setFrais(new BigDecimal("150.00"));
            t.setMontantNet(new BigDecimal("9999.00"));
            t.onCreate();

            assertThat(t.getMontantNet()).isEqualByComparingTo("9999.00");
        }

        @Test
        @DisplayName("onCreate() initialise aussi dateCreation et actif via BaseEntity")
        void onCreateInitialiseBaseEntity() {
            TransactionWave t = new TransactionWave();
            t.setStatutTransaction(StatutTransactionWave.INITIALISE);
            t.setCodeDevise("XOF");
            t.setNombreTentatives(0);
            t.setMontant(new BigDecimal("1000.00"));
            t.setDateCreation(null);
            t.setActif(null);
            t.onCreate();

            assertThat(t.getDateCreation()).isNotNull();
            assertThat(t.getActif()).isTrue();
        }
    }

    // ─── Equals / HashCode / toString ────────────────────────────────────────

    @Nested
    @DisplayName("equals, hashCode et toString")
    class EgaliteEtToString {

        @Test
        @DisplayName("Deux instances avec le même id sont égales")
        void equals_memeId_egales() {
            UUID id = UUID.randomUUID();
            CompteWave c = newCompteWave();

            TransactionWave a = buildTransaction(id, "w1", c);
            TransactionWave b = buildTransaction(id, "w1", c);

            assertThat(a).isEqualTo(b);
            assertThat(a.hashCode()).isEqualTo(b.hashCode());
        }

        @Test
        @DisplayName("Deux instances avec des id différents ne sont pas égales")
        void equals_idsDifferents_pasEgales() {
            TransactionWave a = buildTransaction(UUID.randomUUID(), "w1", newCompteWave());
            TransactionWave b = buildTransaction(UUID.randomUUID(), "w2", newCompteWave());

            assertThat(a).isNotEqualTo(b);
        }

        @Test
        @DisplayName("toString() n'est pas null ni vide")
        void toString_nonNullNonVide() {
            TransactionWave t = baseTransaction();
            assertThat(t.toString()).isNotNull().isNotEmpty();
        }

        @Test
        @DisplayName("toString() contient le waveTransactionId")
        void toString_contientWaveTransactionId() {
            TransactionWave t = baseTransaction();
            t.setWaveTransactionId("wave-toString-test");
            assertThat(t.toString()).contains("wave-toString-test");
        }

        private TransactionWave buildTransaction(UUID id, String waveId, CompteWave compte) {
            TransactionWave t = new TransactionWave();
            t.setId(id);
            t.setWaveTransactionId(waveId);
            t.setTypeTransaction(TypeTransactionWave.DEPOT);
            t.setStatutTransaction(StatutTransactionWave.REUSSIE);
            t.setMontant(BigDecimal.ONE);
            t.setCodeDevise("XOF");
            t.setCompteWave(compte);
            return t;
        }
    }

    // ─── marquerCommeModifie (hérité de BaseEntity) ──────────────────────────

    @Nested
    @DisplayName("marquerCommeModifie() — hérité de BaseEntity")
    class MarquerCommeModifie {

        @Test
        @DisplayName("marquerCommeModifie() met à jour dateModification et modifiePar")
        void marquerCommeModifie_metAJourChamps() {
            TransactionWave t = baseTransaction();
            t.marquerCommeModifie("agent@unionflow.dev");

            assertThat(t.getDateModification()).isNotNull();
            assertThat(t.getModifiePar()).isEqualTo("agent@unionflow.dev");
        }
    }
}
