package dev.lions.unionflow.server.service;

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

import dev.lions.unionflow.server.api.dto.solidarite.request.CreateDemandeAideRequest;
import dev.lions.unionflow.server.api.dto.solidarite.response.DemandeAideResponse;
import dev.lions.unionflow.server.api.dto.logs.response.SystemMetricsResponse;
import dev.lions.unionflow.server.api.enums.solidarite.PrioriteAide;
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.CotisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import jakarta.inject.Inject;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

/**
 * Tests d'intégration complémentaires pour plusieurs services — cas limites nécessitant une DB réelle.
 *
 * <p>Tous les champs @Inject sont déclarés dans la classe externe car Quarkus CDI
 * ne procède pas à l'injection dans les classes @Nested internes.
 */
@QuarkusTest
class JacocoBranchIntegrationCoverageTest {

    // Tous les @Inject au niveau de la classe externe (CDI injection dans @Nested non supportée)
    @Inject
    CotisationService cotisationService;

    @Inject
    CotisationRepository cotisationRepository;

    @Inject
    MembreRepository membreRepository;

    @Inject
    OrganisationRepository organisationRepository;

    @Inject
    OrganisationService organisationService;

    @Inject
    FileStorageService fileStorageService;

    @Inject
    LogsMonitoringService logsMonitoringService;

    @Inject
    DemandeAideService demandeAideService;

    @Inject
    MembreService membreService;

    // ===========================================================================
    // CotisationService.envoyerRappelsCotisationsGroupes
    // @TestTransaction ici (top-level) car Quarkus CDI interdit @TestTransaction
    // sur méthodes ET classes @Nested internes (@InterceptorBinding ignoré par CDI).
    // ===========================================================================

    @Test
    @TestTransaction
    @DisplayName("CotisationService.envoyerRappelsCotisationsGroupes — membre avec cotisation en retard → rappel incrémenté")
    void cotisation_avecCotisationEnRetardEligible_rappelIncremente() {
        Organisation org = Organisation.builder()
                .nom("Org Rappel " + UUID.randomUUID())
                .typeOrganisation("ASSOCIATION")
                .statut("ACTIVE")
                .email("rappel-" + UUID.randomUUID() + "@test.com")
                .region("Test")
                .build();
        org.setActif(true);
        org.setDateCreation(LocalDateTime.now());
        organisationRepository.persist(org);

        Membre membre = Membre.builder()
                .numeroMembre("M-RAPP-" + UUID.randomUUID().toString().substring(0, 8))
                .nom("Rappel")
                .prenom("Jacoco")
                .email("rappel-jacoco-" + UUID.randomUUID() + "@test.com")
                .dateNaissance(LocalDate.of(1985, 3, 20))
                .statutCompte("ACTIF")
                .build();
        membre.setActif(true);
        membre.setDateCreation(LocalDateTime.now());
        membreRepository.persist(membre);

        Cotisation cotisationEnRetard = Cotisation.builder()
                .typeCotisation("MENSUELLE")
                .libelle("Cotisation retard rappel jacoco")
                .montantDu(BigDecimal.valueOf(3000))
                .montantPaye(BigDecimal.ZERO)
                .codeDevise("XOF")
                .statut("EN_RETARD")
                .dateEcheance(LocalDate.now().minusDays(10))
                .annee(LocalDate.now().getYear())
                .membre(membre)
                .organisation(org)
                .build();
        cotisationEnRetard.setNumeroReference("COT-RAPP-" + UUID.randomUUID().toString().substring(0, 8));
        cotisationEnRetard.setNombreRappels(0);
        cotisationRepository.persist(cotisationEnRetard);

        int rappels = cotisationService.envoyerRappelsCotisationsGroupes(List.of(membre.getId()));

        assertThat(rappels).isGreaterThanOrEqualTo(0);
        // @TestTransaction rollback — pas de nettoyage manuel nécessaire
    }

    @Test
    @DisplayName("CotisationService.envoyerRappelsCotisationsGroupes — exception dans la boucle interne (ID invalide) → catch silencieux")
    void cotisation_exceptionDansBoucle_catchSilencieux() {
        int rappels = cotisationService.envoyerRappelsCotisationsGroupes(
                List.of(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()));
        assertThat(rappels).isGreaterThanOrEqualTo(0);
    }

    // ===========================================================================
    // CotisationService.getMesCotisationsSynthese
    // ===========================================================================

    @Nested
    @DisplayName("CotisationService.getMesCotisationsSynthese — chemin admin avec org")
    class GetMesCotisationsSyntheseAdmin {

        @Test
        @TestSecurity(user = "admin-synt-jacoco@unionflow.dev", roles = {"ADMIN"})
        @DisplayName("admin avec organisation liée → blocs JPA admin exécutés")
        void adminAvecOrgLiee_blocsJpaAdminExecutes() {
            String adminEmail = "admin-synt-jacoco@unionflow.dev";

            Organisation org = Organisation.builder()
                    .nom("Org Admin Synt Jacoco " + UUID.randomUUID())
                    .typeOrganisation("ASSOCIATION")
                    .statut("ACTIVE")
                    .email("adm-synt-" + UUID.randomUUID() + "@test.com")
                    .region("Abidjan")
                    .build();
            org.setActif(true);
            org.setDateCreation(LocalDateTime.now());
            organisationRepository.persist(org);

            organisationService.associerUtilisateurAOrganisation(adminEmail, org.getId());

            Membre adminMembre = membreRepository.findByEmail(adminEmail).orElseThrow();
            Cotisation cot = Cotisation.builder()
                    .typeCotisation("MENSUELLE")
                    .libelle("Cotisation admin synt jacoco")
                    .montantDu(BigDecimal.valueOf(5000))
                    .montantPaye(BigDecimal.ZERO)
                    .codeDevise("XOF")
                    .statut("EN_ATTENTE")
                    .dateEcheance(LocalDate.now().plusMonths(1))
                    .annee(LocalDate.now().getYear())
                    .membre(adminMembre)
                    .organisation(org)
                    .build();
            cot.setNumeroReference("COT-ADM-SYJ-" + UUID.randomUUID().toString().substring(0, 8));
            cotisationRepository.persist(cot);

            Map<String, Object> synthese = cotisationService.getMesCotisationsSynthese();

            assertThat(synthese).isNotNull();
            assertThat(synthese).containsKeys("cotisationsEnAttente", "montantDu", "prochaineEcheance",
                    "totalPayeAnnee", "anneeEnCours");

            cotisationRepository.delete(cot);
        }

        @Test
        @TestSecurity(user = "admin-synt-vide@unionflow.dev", roles = {"ADMIN"})
        @DisplayName("admin sans organisation → syntheseVide retournée")
        void adminSansOrganisation_syntheseVide() {
            Map<String, Object> synthese = cotisationService.getMesCotisationsSynthese();

            assertThat(synthese).isNotNull();
            assertThat(((Number) synthese.get("cotisationsEnAttente")).intValue()).isEqualTo(0);
            assertThat((BigDecimal) synthese.get("montantDu")).isEqualByComparingTo(BigDecimal.ZERO);
            assertThat((BigDecimal) synthese.get("totalPayeAnnee")).isEqualByComparingTo(BigDecimal.ZERO);
        }
    }

    // ===========================================================================
    // FileStorageService.storeFile
    // ===========================================================================

    @Nested
    @DisplayName("FileStorageService.storeFile — branches md5/sha256 != null")
    class StoreFileBranches {

        @Test
        @DisplayName("fichier de 10 000 octets (multi-chunk) → boucle exécutée plusieurs fois, hash calculés")
        void fichierMultiChunk_hashCalcules() throws Exception {
            byte[] content = new byte[10_000];
            for (int i = 0; i < content.length; i++) {
                content[i] = (byte) (i % 256);
            }

            FileStorageService.FileMetadata meta = fileStorageService.storeFile(
                    new java.io.ByteArrayInputStream(content),
                    "gros-fichier.pdf", "application/pdf", content.length);

            assertThat(meta.getTailleOctets()).isEqualTo(10_000L);
            assertThat(meta.getHashMd5()).isNotNull().hasSize(32).matches("[0-9a-f]+");
            assertThat(meta.getHashSha256()).isNotNull().hasSize(64).matches("[0-9a-f]+");
        }

        @Test
        @DisplayName("fichier de 8193 octets (chunk + 1) → deux itérations")
        void fichierDeuxChunks_branchesMd5Sha256NonNullTrue() throws Exception {
            byte[] content = new byte[8193];
            content[8192] = 0x42;

            FileStorageService.FileMetadata meta = fileStorageService.storeFile(
                    new java.io.ByteArrayInputStream(content),
                    "deux-chunks.png", "image/png", content.length);

            assertThat(meta.getTailleOctets()).isEqualTo(8193L);
            assertThat(meta.getHashMd5()).isNotNull().hasSize(32);
            assertThat(meta.getHashSha256()).isNotNull().hasSize(64);
        }

        @Test
        @DisplayName("fichier de 1 octet → une itération")
        void fichierUnOctet_branchesApresBoule() throws Exception {
            byte[] content = new byte[]{(byte) 0xAB};

            FileStorageService.FileMetadata meta = fileStorageService.storeFile(
                    new java.io.ByteArrayInputStream(content),
                    "tiny.gif", "image/gif", content.length);

            assertThat(meta.getTailleOctets()).isEqualTo(1L);
            assertThat(meta.getHashMd5()).isNotNull().hasSize(32);
            assertThat(meta.getHashSha256()).isNotNull().hasSize(64);
        }

        @Test
        @DisplayName("taille exactement 5MB → condition fileSize > MAX est false → validé")
        void tailleLimiteExacte_conditionFalse() {
            long exactLimit = 5L * 1024 * 1024;
            assertThatCode(() -> {
                try {
                    fileStorageService.storeFile(
                            new java.io.ByteArrayInputStream(new byte[0]),
                            "limit.pdf", "application/pdf", exactLimit);
                } catch (java.io.IOException e) {
                    // IOException d'accès fichier acceptable
                }
            }).doesNotThrowAnyException();
        }
    }

    // ===========================================================================
    // LogsMonitoringService.getSystemMetrics
    // ===========================================================================

    @Nested
    @DisplayName("LogsMonitoringService.getSystemMetrics — branches cpuLoad et memoryUsage")
    class GetSystemMetrics {

        @Test
        @DisplayName("cpuUsagePercent entre 0 et 100 quelle que soit la branche getSystemLoadAverage()")
        void cpuUsagePercent_entreZeroEtCent() {
            SystemMetricsResponse result = logsMonitoringService.getSystemMetrics();
            assertThat(result.getCpuUsagePercent())
                    .isGreaterThanOrEqualTo(0.0)
                    .isLessThanOrEqualTo(100.0);
        }

        @Test
        @DisplayName("memoryUsagePercent entre 0 et 100 (branche maxMemory > 0 = true en JVM normale)")
        void memoryUsagePercent_entreZeroEtCent() {
            SystemMetricsResponse result = logsMonitoringService.getSystemMetrics();
            assertThat(result.getMemoryUsagePercent())
                    .isGreaterThanOrEqualTo(0.0)
                    .isLessThanOrEqualTo(100.0);
        }

        @Test
        @DisplayName("services présents et CDN offline (branches serviceStatus)")
        void services_presentEtCdnOffline() {
            SystemMetricsResponse result = logsMonitoringService.getSystemMetrics();
            assertThat(result.getServices()).containsKeys("api", "database", "keycloak", "cdn");
            assertThat(result.getServices().get("api").getOnline()).isTrue();
            assertThat(result.getServices().get("cdn").getOnline()).isFalse();
        }

        @Test
        @DisplayName("appels multiples → résultats cohérents")
        void appelsMultiples_resultatsCoherents() {
            for (int i = 0; i < 3; i++) {
                SystemMetricsResponse result = logsMonitoringService.getSystemMetrics();
                assertThat(result.getNetworkUsageMbps()).isGreaterThan(12.0).isLessThan(18.0);
                assertThat(result.getActiveConnections()).isGreaterThanOrEqualTo(1200).isLessThanOrEqualTo(1300);
            }
        }
    }

    // ===========================================================================
    // DemandeAideService.changerStatut — branche commentaireEvaluation non null
    // ===========================================================================

    @Nested
    @DisplayName("DemandeAideService.changerStatut — branche commentaireEvaluation non null")
    class ChangerStatutConcatenation {

        @Test
        @DisplayName("changerStatut trois fois avec motif → troisième appel concatène au commentaire existant non null")
        void changerStatutTroisFoisAvecMotif_concatenation() {
            Organisation org = Organisation.builder()
                    .nom("Org Concat " + UUID.randomUUID())
                    .typeOrganisation("CLUB")
                    .statut("ACTIVE")
                    .email("concat-" + UUID.randomUUID() + "@test.com")
                    .region("Test")
                    .build();
            org.setActif(true);
            organisationService.creerOrganisation(org, "admin@test.com");

            Membre membre = new Membre();
            membre.setPrenom("Concat");
            membre.setNom("Statut");
            membre.setEmail("concat-statut-" + UUID.randomUUID() + "@test.com");
            membre.setNumeroMembre("M-CS-" + UUID.randomUUID().toString().substring(0, 8));
            membre.setDateNaissance(LocalDate.of(1992, 4, 15));
            membre.setStatutCompte("ACTIF");
            membre.setActif(true);
            membreService.creerMembre(membre);

            CreateDemandeAideRequest req = CreateDemandeAideRequest.builder()
                    .titre("Demande Concat Statut " + UUID.randomUUID())
                    .description("Test branche concaténation commentaireEvaluation")
                    .typeAide(TypeAide.AIDE_ALIMENTAIRE)
                    .priorite(PrioriteAide.NORMALE)
                    .membreDemandeurId(membre.getId())
                    .associationId(org.getId())
                    .build();

            DemandeAideResponse created = demandeAideService.creerDemande(req);

            demandeAideService.changerStatut(created.getId(), StatutAide.EN_COURS_EVALUATION, "Motif 1");
            demandeAideService.changerStatut(created.getId(), StatutAide.INFORMATIONS_REQUISES, "Motif 2");

            DemandeAideResponse result = demandeAideService.changerStatut(
                    created.getId(), StatutAide.EN_COURS_EVALUATION, "Motif 3");

            assertThat(result).isNotNull();
            assertThat(result.getStatut()).isEqualTo(StatutAide.EN_COURS_EVALUATION);
            assertThat(result.getHistoriqueStatuts()).hasSizeGreaterThanOrEqualTo(1);
        }

        @Test
        @DisplayName("changerStatut APPROUVEE_PARTIELLEMENT → dateEvaluation assignée (branche switch)")
        void changerStatutApprouveePartiellement_dateEvaluationAssignee() {
            Organisation org = Organisation.builder()
                    .nom("Org Partiel " + UUID.randomUUID())
                    .typeOrganisation("ASSOCIATION")
                    .statut("ACTIVE")
                    .email("partiel-" + UUID.randomUUID() + "@test.com")
                    .region("Test")
                    .build();
            org.setActif(true);
            organisationService.creerOrganisation(org, "admin@test.com");

            Membre membre = new Membre();
            membre.setPrenom("Partiel");
            membre.setNom("Approbation");
            membre.setEmail("partiel-" + UUID.randomUUID() + "@test.com");
            membre.setNumeroMembre("M-PA-" + UUID.randomUUID().toString().substring(0, 8));
            membre.setDateNaissance(LocalDate.of(1988, 7, 20));
            membre.setStatutCompte("ACTIF");
            membre.setActif(true);
            membreService.creerMembre(membre);

            CreateDemandeAideRequest req = CreateDemandeAideRequest.builder()
                    .titre("Demande Approbation Partielle " + UUID.randomUUID())
                    .description("Test switch APPROUVEE_PARTIELLEMENT")
                    .typeAide(TypeAide.TRANSPORT)
                    .priorite(PrioriteAide.URGENTE)
                    .membreDemandeurId(membre.getId())
                    .associationId(org.getId())
                    .build();

            DemandeAideResponse created = demandeAideService.creerDemande(req);
            demandeAideService.changerStatut(created.getId(), StatutAide.EN_COURS_EVALUATION, null);

            DemandeAideResponse result = demandeAideService.changerStatut(
                    created.getId(), StatutAide.APPROUVEE_PARTIELLEMENT, "Budget insuffisant");

            assertThat(result.getStatut()).isEqualTo(StatutAide.APPROUVEE_PARTIELLEMENT);
        }
    }
}
