package dev.lions.unionflow.server.entity;

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("Paiement entity")
class PaiementTest {

    // -------------------------------------------------------------------------
    // No-args constructor
    // -------------------------------------------------------------------------
    @Test
    @DisplayName("no-args constructor creates instance with null/default fields")
    void noArgsConstructor_createsInstance() {
        Paiement paiement = new Paiement();

        assertThat(paiement).isNotNull();
        assertThat(paiement.getNumeroReference()).isNull();
        assertThat(paiement.getMontant()).isNull();
        assertThat(paiement.getCodeDevise()).isNull();
        assertThat(paiement.getMethodePaiement()).isNull();
        // statutPaiement default is only set via @Builder.Default — not through no-args
        assertThat(paiement.getMembre()).isNull();
    }

    // -------------------------------------------------------------------------
    // Builder
    // -------------------------------------------------------------------------
    @Nested
    @DisplayName("Builder")
    class BuilderTests {

        @Test
        @DisplayName("builder default statutPaiement is EN_ATTENTE")
        void builder_defaultStatutPaiementIsEnAttente() {
            Paiement paiement = Paiement.builder()
                    .numeroReference("PAY-2026-000001")
                    .montant(BigDecimal.valueOf(5000))
                    .codeDevise("XOF")
                    .methodePaiement("WAVE")
                    .build();

            assertThat(paiement.getStatutPaiement()).isEqualTo("EN_ATTENTE");
        }

        @Test
        @DisplayName("builder default paiementsObjets is empty list")
        void builder_defaultPaiementsObjetsIsEmptyList() {
            Paiement paiement = Paiement.builder().build();

            assertThat(paiement.getPaiementsObjets()).isNotNull().isEmpty();
        }

        @Test
        @DisplayName("builder sets all explicit fields")
        void builder_setsAllExplicitFields() {
            LocalDateTime now = LocalDateTime.now();
            Membre membre = new Membre();

            Paiement paiement = Paiement.builder()
                    .numeroReference("PAY-2026-000042")
                    .montant(BigDecimal.valueOf(10000, 2))
                    .codeDevise("XOF")
                    .methodePaiement("ORANGE_MONEY")
                    .statutPaiement("VALIDE")
                    .datePaiement(now)
                    .dateValidation(now.plusMinutes(5))
                    .validateur("admin@unionflow.com")
                    .referenceExterne("EXT-REF-001")
                    .urlPreuve("https://example.com/preuve.jpg")
                    .commentaire("Paiement cotisation mars 2026")
                    .ipAddress("192.168.1.1")
                    .userAgent("Mozilla/5.0")
                    .membre(membre)
                    .paiementsObjets(new ArrayList<>())
                    .build();

            assertThat(paiement.getNumeroReference()).isEqualTo("PAY-2026-000042");
            assertThat(paiement.getMontant()).isEqualByComparingTo(BigDecimal.valueOf(10000, 2));
            assertThat(paiement.getCodeDevise()).isEqualTo("XOF");
            assertThat(paiement.getMethodePaiement()).isEqualTo("ORANGE_MONEY");
            assertThat(paiement.getStatutPaiement()).isEqualTo("VALIDE");
            assertThat(paiement.getDatePaiement()).isEqualTo(now);
            assertThat(paiement.getDateValidation()).isEqualTo(now.plusMinutes(5));
            assertThat(paiement.getValidateur()).isEqualTo("admin@unionflow.com");
            assertThat(paiement.getReferenceExterne()).isEqualTo("EXT-REF-001");
            assertThat(paiement.getUrlPreuve()).isEqualTo("https://example.com/preuve.jpg");
            assertThat(paiement.getCommentaire()).isEqualTo("Paiement cotisation mars 2026");
            assertThat(paiement.getIpAddress()).isEqualTo("192.168.1.1");
            assertThat(paiement.getUserAgent()).isEqualTo("Mozilla/5.0");
            assertThat(paiement.getMembre()).isSameAs(membre);
        }
    }

    // -------------------------------------------------------------------------
    // Getters / Setters
    // -------------------------------------------------------------------------
    @Nested
    @DisplayName("Getters and Setters")
    class GettersSettersTests {

        @Test
        @DisplayName("setNumeroReference / getNumeroReference round-trips")
        void numeroReference_roundTrips() {
            Paiement p = new Paiement();
            p.setNumeroReference("PAY-TEST-001");
            assertThat(p.getNumeroReference()).isEqualTo("PAY-TEST-001");
        }

        @Test
        @DisplayName("setMontant / getMontant round-trips")
        void montant_roundTrips() {
            Paiement p = new Paiement();
            p.setMontant(BigDecimal.valueOf(25000));
            assertThat(p.getMontant()).isEqualByComparingTo("25000");
        }

        @Test
        @DisplayName("setCodeDevise / getCodeDevise round-trips")
        void codeDevise_roundTrips() {
            Paiement p = new Paiement();
            p.setCodeDevise("EUR");
            assertThat(p.getCodeDevise()).isEqualTo("EUR");
        }

        @Test
        @DisplayName("setMethodePaiement / getMethodePaiement round-trips")
        void methodePaiement_roundTrips() {
            Paiement p = new Paiement();
            p.setMethodePaiement("VIREMENT");
            assertThat(p.getMethodePaiement()).isEqualTo("VIREMENT");
        }

        @Test
        @DisplayName("setStatutPaiement / getStatutPaiement round-trips")
        void statutPaiement_roundTrips() {
            Paiement p = new Paiement();
            p.setStatutPaiement("ANNULE");
            assertThat(p.getStatutPaiement()).isEqualTo("ANNULE");
        }

        @Test
        @DisplayName("setDatePaiement / getDatePaiement round-trips")
        void datePaiement_roundTrips() {
            LocalDateTime dt = LocalDateTime.of(2026, 4, 1, 10, 0);
            Paiement p = new Paiement();
            p.setDatePaiement(dt);
            assertThat(p.getDatePaiement()).isEqualTo(dt);
        }

        @Test
        @DisplayName("setDateValidation / getDateValidation round-trips")
        void dateValidation_roundTrips() {
            LocalDateTime dt = LocalDateTime.of(2026, 4, 1, 11, 0);
            Paiement p = new Paiement();
            p.setDateValidation(dt);
            assertThat(p.getDateValidation()).isEqualTo(dt);
        }

        @Test
        @DisplayName("setValidateur / getValidateur round-trips")
        void validateur_roundTrips() {
            Paiement p = new Paiement();
            p.setValidateur("tresorier@example.com");
            assertThat(p.getValidateur()).isEqualTo("tresorier@example.com");
        }

        @Test
        @DisplayName("setReferenceExterne / getReferenceExterne round-trips")
        void referenceExterne_roundTrips() {
            Paiement p = new Paiement();
            p.setReferenceExterne("TXN-98765");
            assertThat(p.getReferenceExterne()).isEqualTo("TXN-98765");
        }

        @Test
        @DisplayName("setUrlPreuve / getUrlPreuve round-trips")
        void urlPreuve_roundTrips() {
            Paiement p = new Paiement();
            p.setUrlPreuve("https://storage.example.com/preuves/p1.jpg");
            assertThat(p.getUrlPreuve()).isEqualTo("https://storage.example.com/preuves/p1.jpg");
        }

        @Test
        @DisplayName("setCommentaire / getCommentaire round-trips")
        void commentaire_roundTrips() {
            Paiement p = new Paiement();
            p.setCommentaire("Commentaire test");
            assertThat(p.getCommentaire()).isEqualTo("Commentaire test");
        }

        @Test
        @DisplayName("setIpAddress / getIpAddress round-trips")
        void ipAddress_roundTrips() {
            Paiement p = new Paiement();
            p.setIpAddress("10.0.0.1");
            assertThat(p.getIpAddress()).isEqualTo("10.0.0.1");
        }

        @Test
        @DisplayName("setUserAgent / getUserAgent round-trips")
        void userAgent_roundTrips() {
            Paiement p = new Paiement();
            p.setUserAgent("TestAgent/1.0");
            assertThat(p.getUserAgent()).isEqualTo("TestAgent/1.0");
        }

        @Test
        @DisplayName("setMembre / getMembre round-trips")
        void membre_roundTrips() {
            Membre membre = new Membre();
            Paiement p = new Paiement();
            p.setMembre(membre);
            assertThat(p.getMembre()).isSameAs(membre);
        }

        @Test
        @DisplayName("setPaiementsObjets / getPaiementsObjets round-trips")
        void paiementsObjets_roundTrips() {
            List<PaiementObjet> objets = new ArrayList<>();
            Paiement p = new Paiement();
            p.setPaiementsObjets(objets);
            assertThat(p.getPaiementsObjets()).isSameAs(objets);
        }

        @Test
        @DisplayName("setTransactionWave / getTransactionWave round-trips")
        void transactionWave_roundTrips() {
            TransactionWave tw = new TransactionWave();
            Paiement p = new Paiement();
            p.setTransactionWave(tw);
            assertThat(p.getTransactionWave()).isSameAs(tw);
        }
    }

    // -------------------------------------------------------------------------
    // UUID id (inherited from BaseEntity)
    // -------------------------------------------------------------------------
    @Test
    @DisplayName("UUID id field is settable and gettable")
    void uuidId_settableAndGettable() {
        UUID id = UUID.randomUUID();
        Paiement p = new Paiement();
        p.setId(id);
        assertThat(p.getId()).isEqualTo(id);
    }

    // -------------------------------------------------------------------------
    // Business method: isValide()
    // -------------------------------------------------------------------------
    @Nested
    @DisplayName("isValide()")
    class IsValideTests {

        @Test
        @DisplayName("returns true when statutPaiement is VALIDE")
        void isValide_returnsTrue_whenValide() {
            Paiement p = Paiement.builder().statutPaiement("VALIDE").build();
            assertThat(p.isValide()).isTrue();
        }

        @Test
        @DisplayName("returns false when statutPaiement is EN_ATTENTE")
        void isValide_returnsFalse_whenEnAttente() {
            Paiement p = Paiement.builder().build(); // default EN_ATTENTE
            assertThat(p.isValide()).isFalse();
        }

        @Test
        @DisplayName("returns false when statutPaiement is ANNULE")
        void isValide_returnsFalse_whenAnnule() {
            Paiement p = Paiement.builder().statutPaiement("ANNULE").build();
            assertThat(p.isValide()).isFalse();
        }

        @Test
        @DisplayName("returns false when statutPaiement is REJETE")
        void isValide_returnsFalse_whenRejete() {
            Paiement p = Paiement.builder().statutPaiement("REJETE").build();
            assertThat(p.isValide()).isFalse();
        }
    }

    // -------------------------------------------------------------------------
    // Business method: peutEtreModifie()
    // -------------------------------------------------------------------------
    @Nested
    @DisplayName("peutEtreModifie()")
    class PeutEtreModifieTests {

        @Test
        @DisplayName("returns true when statutPaiement is EN_ATTENTE")
        void peutEtreModifie_returnsTrue_whenEnAttente() {
            Paiement p = Paiement.builder().build(); // default EN_ATTENTE
            assertThat(p.peutEtreModifie()).isTrue();
        }

        @Test
        @DisplayName("returns false when statutPaiement is VALIDE")
        void peutEtreModifie_returnsFalse_whenValide() {
            Paiement p = Paiement.builder().statutPaiement("VALIDE").build();
            assertThat(p.peutEtreModifie()).isFalse();
        }

        @Test
        @DisplayName("returns false when statutPaiement is ANNULE")
        void peutEtreModifie_returnsFalse_whenAnnule() {
            Paiement p = Paiement.builder().statutPaiement("ANNULE").build();
            assertThat(p.peutEtreModifie()).isFalse();
        }

        @Test
        @DisplayName("returns true when statutPaiement is REJETE")
        void peutEtreModifie_returnsTrue_whenRejete() {
            Paiement p = Paiement.builder().statutPaiement("REJETE").build();
            assertThat(p.peutEtreModifie()).isTrue();
        }
    }

    // -------------------------------------------------------------------------
    // Static factory: genererNumeroReference()
    // -------------------------------------------------------------------------
    @Test
    @DisplayName("genererNumeroReference returns a non-null string matching PAY-YYYY-<digits> pattern")
    void genererNumeroReference_returnsValidString() {
        String ref = Paiement.genererNumeroReference();

        assertThat(ref).isNotNull();
        assertThat(ref).startsWith("PAY-");
        assertThat(ref).matches("PAY-\\d{4}-\\d{12}");
    }

    @Test
    @DisplayName("genererNumeroReference generates unique values on successive calls")
    void genererNumeroReference_isUnique() throws InterruptedException {
        String ref1 = Paiement.genererNumeroReference();
        Thread.sleep(1); // ensure millis differ
        String ref2 = Paiement.genererNumeroReference();
        // They may collide on the same millisecond modulo, so just verify format
        assertThat(ref1).matches("PAY-\\d{4}-\\d{12}");
        assertThat(ref2).matches("PAY-\\d{4}-\\d{12}");
    }
}
