package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.entity.BaremeCotisationRole;
import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.MembreOrganisation;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.ParametresCotisationOrganisation;
import dev.lions.unionflow.server.repository.BaremeCotisationRoleRepository;
import dev.lions.unionflow.server.repository.CotisationRepository;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import dev.lions.unionflow.server.repository.ParametresCotisationOrganisationRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.math.BigDecimal;
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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

/**
 * Tests unitaires pour {@link CotisationAutoGenerationService}.
 * Couverture 100% lignes/branches/méthodes.
 */
@DisplayName("Tests CotisationAutoGenerationService")
class CotisationAutoGenerationServiceTest {

    @InjectMocks
    CotisationAutoGenerationService service;

    @Mock
    ParametresCotisationOrganisationRepository parametresRepository;

    @Mock
    MembreOrganisationRepository membreOrganisationRepository;

    @Mock
    BaremeCotisationRoleRepository baremeRepository;

    @Mock
    CotisationRepository cotisationRepository;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

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

    private Organisation buildOrg(String nom) {
        Organisation org = new Organisation();
        org.setId(UUID.randomUUID());
        org.setNom(nom);
        return org;
    }

    private Membre buildMembre() {
        Membre m = new Membre();
        m.setId(UUID.randomUUID());
        return m;
    }

    private MembreOrganisation buildMO(Organisation org, Membre membre, String roleOrg) {
        MembreOrganisation mo = new MembreOrganisation();
        mo.setId(UUID.randomUUID());
        mo.setOrganisation(org);
        mo.setMembre(membre);
        mo.setRoleOrg(roleOrg);
        return mo;
    }

    private ParametresCotisationOrganisation buildParams(Organisation org,
                                                          BigDecimal montantMensuel,
                                                          String devise) {
        ParametresCotisationOrganisation p = new ParametresCotisationOrganisation();
        p.setOrganisation(org);
        p.setMontantCotisationMensuelle(montantMensuel);
        p.setDevise(devise);
        return p;
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  genererCotisationsMensuelles
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("genererCotisationsMensuelles")
    class GenererCotisationsMensuelles {

        @Test
        @DisplayName("Aucune organisation avec génération automatique activée — retour immédiat")
        void aucuneOrganisationAvecGenerationActivee_retourImmediat() {
            when(parametresRepository.findAvecGenerationAutomatiqueActivee())
                    .thenReturn(Collections.emptyList());

            service.genererCotisationsMensuelles();

            verify(parametresRepository).findAvecGenerationAutomatiqueActivee();
            verifyNoInteractions(membreOrganisationRepository, cotisationRepository, baremeRepository);
        }

        @Test
        @DisplayName("Une organisation avec membres actifs — cotisations créées")
        void uneOrganisationAvecMembresActifs_cotisationsCreees() {
            Organisation org = buildOrg("Org Test");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, "MEMBRE_ORDINAIRE");
            ParametresCotisationOrganisation params = buildParams(org, new BigDecimal("5000"), "XOF");

            when(parametresRepository.findAvecGenerationAutomatiqueActivee())
                    .thenReturn(List.of(params));
            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);
            when(baremeRepository.findByOrganisationIdAndRoleOrg(any(), any()))
                    .thenReturn(Optional.empty());

            service.genererCotisationsMensuelles();

            verify(cotisationRepository).persist(any(Cotisation.class));
        }

        @Test
        @DisplayName("Plusieurs organisations — totaux accumulés correctement")
        void plusieursOrganisations_totauxAccumules() {
            Organisation org1 = buildOrg("Org1");
            Organisation org2 = buildOrg("Org2");
            Membre m1 = buildMembre();
            Membre m2 = buildMembre();
            MembreOrganisation mo1 = buildMO(org1, m1, null);
            MembreOrganisation mo2 = buildMO(org2, m2, null);
            ParametresCotisationOrganisation p1 = buildParams(org1, new BigDecimal("1000"), "XOF");
            ParametresCotisationOrganisation p2 = buildParams(org2, new BigDecimal("2000"), "EUR");

            when(parametresRepository.findAvecGenerationAutomatiqueActivee())
                    .thenReturn(List.of(p1, p2));
            when(membreOrganisationRepository.findMembresActifsParOrganisation(org1.getId()))
                    .thenReturn(List.of(mo1));
            when(membreOrganisationRepository.findMembresActifsParOrganisation(org2.getId()))
                    .thenReturn(List.of(mo2));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);
            when(baremeRepository.findByOrganisationIdAndRoleOrg(any(), any()))
                    .thenReturn(Optional.empty());

            service.genererCotisationsMensuelles();

            verify(cotisationRepository, times(2)).persist(any(Cotisation.class));
        }
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  genererPourOrganisation
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("genererPourOrganisation")
    class GenererPourOrganisation {

        @Test
        @DisplayName("Aucun membre actif — retourne [0, 0]")
        void aucunMembreActif_retourneZeroZero() {
            Organisation org = buildOrg("Vide");
            ParametresCotisationOrganisation params = buildParams(org, new BigDecimal("1000"), "XOF");

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(Collections.emptyList());

            int[] result = service.genererPourOrganisation(org, params, 2026, 4);

            assertThat(result).containsExactly(0, 0);
            verifyNoInteractions(cotisationRepository, baremeRepository);
        }

        @Test
        @DisplayName("Membre avec mo.getMembre() == null — ignoré (idempotence skip)")
        void membreNull_ignore() {
            Organisation org = buildOrg("Org");
            ParametresCotisationOrganisation params = buildParams(org, new BigDecimal("1000"), "XOF");

            MembreOrganisation moSansMembre = new MembreOrganisation();
            moSansMembre.setId(UUID.randomUUID());
            moSansMembre.setOrganisation(org);
            moSansMembre.setMembre(null); // Membre null

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(moSansMembre));

            int[] result = service.genererPourOrganisation(org, params, 2026, 4);

            assertThat(result).containsExactly(0, 0);
            verifyNoInteractions(cotisationRepository, baremeRepository);
        }

        @Test
        @DisplayName("Cotisation déjà existante pour ce mois — ignorée (idempotence)")
        void cotisationDejaExistante_ignoree() {
            Organisation org = buildOrg("Org");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, "TRESORIER");
            ParametresCotisationOrganisation params = buildParams(org, new BigDecimal("2000"), "XOF");

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    membre.getId(), org.getId(), 2026, 4)).thenReturn(true);

            int[] result = service.genererPourOrganisation(org, params, 2026, 4);

            assertThat(result[0]).isZero(); // 0 créées
            assertThat(result[1]).isEqualTo(1); // 1 ignorée
            verify(cotisationRepository, never()).persist(any(Cotisation.class));
        }

        @Test
        @DisplayName("Montant résolu à null — cotisation ignorée")
        void montantNull_cotisationIgnoree() {
            Organisation org = buildOrg("Org");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, "PRESIDENT");
            // montantCotisationMensuelle = null (params sans montant)
            ParametresCotisationOrganisation params = new ParametresCotisationOrganisation();
            params.setOrganisation(org);
            params.setMontantCotisationMensuelle(null);
            params.setDevise("XOF");

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);
            when(baremeRepository.findByOrganisationIdAndRoleOrg(any(), any()))
                    .thenReturn(Optional.empty());

            int[] result = service.genererPourOrganisation(org, params, 2026, 4);

            assertThat(result[0]).isZero();
            assertThat(result[1]).isEqualTo(1); // ignorée car montant null
            verify(cotisationRepository, never()).persist(any(Cotisation.class));
        }

        @Test
        @DisplayName("Montant résolu à zéro — cotisation ignorée")
        void montantZero_cotisationIgnoree() {
            Organisation org = buildOrg("Org");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, "SECRETAIRE");
            ParametresCotisationOrganisation params = buildParams(org, BigDecimal.ZERO, "XOF");

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);
            when(baremeRepository.findByOrganisationIdAndRoleOrg(any(), any()))
                    .thenReturn(Optional.empty());

            int[] result = service.genererPourOrganisation(org, params, 2026, 4);

            assertThat(result[0]).isZero();
            assertThat(result[1]).isEqualTo(1);
            verify(cotisationRepository, never()).persist(any(Cotisation.class));
        }

        @Test
        @DisplayName("Cotisation créée — champs de la cotisation correctement renseignés")
        void cotisationCreee_champsCorrects() {
            Organisation org = buildOrg("AssocTest");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, null);
            ParametresCotisationOrganisation params = buildParams(org, new BigDecimal("3000"), "XOF");

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);

            int[] result = service.genererPourOrganisation(org, params, 2026, 4);

            assertThat(result[0]).isEqualTo(1);
            assertThat(result[1]).isZero();

            ArgumentCaptor<Cotisation> captor = ArgumentCaptor.forClass(Cotisation.class);
            verify(cotisationRepository).persist(captor.capture());

            Cotisation cot = captor.getValue();
            assertThat(cot.getTypeCotisation()).isEqualTo("MENSUELLE");
            assertThat(cot.getStatut()).isEqualTo("EN_ATTENTE");
            assertThat(cot.getMontantDu()).isEqualByComparingTo(new BigDecimal("3000"));
            assertThat(cot.getMontantPaye()).isEqualByComparingTo(BigDecimal.ZERO);
            assertThat(cot.getCodeDevise()).isEqualTo("XOF");
            assertThat(cot.getAnnee()).isEqualTo(2026);
            assertThat(cot.getMois()).isEqualTo(4);
            assertThat(cot.getRecurrente()).isTrue();
            assertThat(cot.getMembre()).isEqualTo(membre);
            assertThat(cot.getOrganisation()).isEqualTo(org);
            assertThat(cot.getLibelle()).contains("04/2026");
            assertThat(cot.getDescription()).contains("AssocTest");
            assertThat(cot.getNumeroReference()).isNotBlank();
        }

        @Test
        @DisplayName("Devise null dans params — utilise 'XOF' par défaut")
        void deviseNull_utilisexof() {
            Organisation org = buildOrg("Org");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, null);
            ParametresCotisationOrganisation params = buildParams(org, new BigDecimal("1000"), null); // devise null

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);

            service.genererPourOrganisation(org, params, 2026, 1);

            ArgumentCaptor<Cotisation> captor = ArgumentCaptor.forClass(Cotisation.class);
            verify(cotisationRepository).persist(captor.capture());
            assertThat(captor.getValue().getCodeDevise()).isEqualTo("XOF");
        }

        @Test
        @DisplayName("Plusieurs membres — accumule crees et ignores correctement")
        void plusieursMembres_accumulCorrectement() {
            Organisation org = buildOrg("Org");
            Membre m1 = buildMembre();
            Membre m2 = buildMembre();
            Membre m3 = buildMembre();
            MembreOrganisation mo1 = buildMO(org, m1, null);
            MembreOrganisation mo2 = buildMO(org, m2, null);
            MembreOrganisation mo3 = buildMO(org, m3, null);
            ParametresCotisationOrganisation params = buildParams(org, new BigDecimal("1000"), "XOF");

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo1, mo2, mo3));
            // m1 déjà existant, m2 créé, m3 créé
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    eq(m1.getId()), any(), anyInt(), anyInt())).thenReturn(true);
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    eq(m2.getId()), any(), anyInt(), anyInt())).thenReturn(false);
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    eq(m3.getId()), any(), anyInt(), anyInt())).thenReturn(false);

            int[] result = service.genererPourOrganisation(org, params, 2026, 3);

            assertThat(result[0]).isEqualTo(2); // 2 créées
            assertThat(result[1]).isEqualTo(1); // 1 ignorée (existante)
            verify(cotisationRepository, times(2)).persist(any(Cotisation.class));
        }
    }

    // ═════════════════════════════════════════════════════════════════════════
    //  resoudreMontantMensuel (tested indirectly via genererPourOrganisation)
    // ═════════════════════════════════════════════════════════════════════════

    @Nested
    @DisplayName("resoudreMontantMensuel — résolution du montant")
    class ResoudreMontantMensuel {

        @Test
        @DisplayName("roleOrg null — utilise montant par défaut de params")
        void roleOrgNull_utiliseMontantParDefaut() {
            Organisation org = buildOrg("Org");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, null);
            BigDecimal montantDefaut = new BigDecimal("4500");
            ParametresCotisationOrganisation params = buildParams(org, montantDefaut, "XOF");

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);

            service.genererPourOrganisation(org, params, 2026, 5);

            ArgumentCaptor<Cotisation> captor = ArgumentCaptor.forClass(Cotisation.class);
            verify(cotisationRepository).persist(captor.capture());
            assertThat(captor.getValue().getMontantDu()).isEqualByComparingTo(montantDefaut);
            // baremeRepository ne doit pas être appelé si roleOrg est null
            verify(baremeRepository, never()).findByOrganisationIdAndRoleOrg(any(), any());
        }

        @Test
        @DisplayName("roleOrg blank — utilise montant par défaut (branche isBlank)")
        void roleOrgBlank_utiliseMontantParDefaut() {
            Organisation org = buildOrg("Org");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, "   "); // blank
            BigDecimal montantDefaut = new BigDecimal("2000");
            ParametresCotisationOrganisation params = buildParams(org, montantDefaut, "XOF");

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);

            service.genererPourOrganisation(org, params, 2026, 5);

            ArgumentCaptor<Cotisation> captor = ArgumentCaptor.forClass(Cotisation.class);
            verify(cotisationRepository).persist(captor.capture());
            assertThat(captor.getValue().getMontantDu()).isEqualByComparingTo(montantDefaut);
            verify(baremeRepository, never()).findByOrganisationIdAndRoleOrg(any(), any());
        }

        @Test
        @DisplayName("roleOrg présent, barème trouvé avec montant — utilise montant du barème")
        void roleOrgPresent_baremesTrouve_utiliseMontantBareme() {
            Organisation org = buildOrg("Org");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, "PRESIDENT");
            BigDecimal montantBareme = new BigDecimal("10000");
            ParametresCotisationOrganisation params = buildParams(org, new BigDecimal("1000"), "XOF");

            BaremeCotisationRole bareme = new BaremeCotisationRole();
            bareme.setMontantMensuel(montantBareme);

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);
            when(baremeRepository.findByOrganisationIdAndRoleOrg(org.getId(), "PRESIDENT"))
                    .thenReturn(Optional.of(bareme));

            service.genererPourOrganisation(org, params, 2026, 5);

            ArgumentCaptor<Cotisation> captor = ArgumentCaptor.forClass(Cotisation.class);
            verify(cotisationRepository).persist(captor.capture());
            assertThat(captor.getValue().getMontantDu()).isEqualByComparingTo(montantBareme);
        }

        @Test
        @DisplayName("roleOrg présent, barème trouvé mais montantMensuel null — utilise montant défaut")
        void roleOrgPresent_baremesMontantNull_utiliseMontantDefaut() {
            Organisation org = buildOrg("Org");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, "TRESORIER");
            BigDecimal montantDefaut = new BigDecimal("7500");
            ParametresCotisationOrganisation params = buildParams(org, montantDefaut, "XOF");

            BaremeCotisationRole baremeAvecMontantNull = new BaremeCotisationRole();
            baremeAvecMontantNull.setMontantMensuel(null); // montant null dans barème

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);
            when(baremeRepository.findByOrganisationIdAndRoleOrg(org.getId(), "TRESORIER"))
                    .thenReturn(Optional.of(baremeAvecMontantNull));

            service.genererPourOrganisation(org, params, 2026, 5);

            ArgumentCaptor<Cotisation> captor = ArgumentCaptor.forClass(Cotisation.class);
            verify(cotisationRepository).persist(captor.capture());
            assertThat(captor.getValue().getMontantDu()).isEqualByComparingTo(montantDefaut);
        }

        @Test
        @DisplayName("roleOrg présent, barème absent — utilise montant par défaut")
        void roleOrgPresent_baremeAbsent_utiliseMontantDefaut() {
            Organisation org = buildOrg("Org");
            Membre membre = buildMembre();
            MembreOrganisation mo = buildMO(org, membre, "SECRETAIRE");
            BigDecimal montantDefaut = new BigDecimal("5000");
            ParametresCotisationOrganisation params = buildParams(org, montantDefaut, "XOF");

            when(membreOrganisationRepository.findMembresActifsParOrganisation(org.getId()))
                    .thenReturn(List.of(mo));
            when(cotisationRepository.existsByMembreOrganisationAnneeAndMois(
                    any(), any(), anyInt(), anyInt())).thenReturn(false);
            when(baremeRepository.findByOrganisationIdAndRoleOrg(org.getId(), "SECRETAIRE"))
                    .thenReturn(Optional.empty());

            service.genererPourOrganisation(org, params, 2026, 5);

            ArgumentCaptor<Cotisation> captor = ArgumentCaptor.forClass(Cotisation.class);
            verify(cotisationRepository).persist(captor.capture());
            assertThat(captor.getValue().getMontantDu()).isEqualByComparingTo(montantDefaut);
        }
    }
}
