package dev.lions.unionflow.server.service;

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

import dev.lions.unionflow.server.api.dto.solidarite.request.CreatePropositionAideRequest;
import dev.lions.unionflow.server.api.dto.solidarite.response.DemandeAideResponse;
import dev.lions.unionflow.server.api.dto.solidarite.response.PropositionAideResponse;
import dev.lions.unionflow.server.api.enums.solidarite.StatutProposition;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import io.quarkus.arc.ClientProxy;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

/**
 * Tests complémentaires ciblant les branches non couvertes dans
 * {@link PropositionAideService} :
 *
 * <ul>
 *   <li><b>L168-169</b> — branche {@code if (response != null)} dans {@code obtenirParId()} :
 *       {@code ajouterAuCache(response)} et {@code ajouterAIndex(response)}.
 *       Ces lignes sont <em>du code mort</em> car {@code simulerRecuperationBDD(id)} retourne
 *       <em>toujours</em> {@code null} (méthode stub vide, L487-490).  Sans modifier le code
 *       source ni la visibilité de la méthode, il est <b>impossible</b> de couvrir ces lignes
 *       par des tests unitaires/d'intégration standard.</li>
 *   <li><b>L256</b> — branche {@code filter(p -> score >= 30.0)} (côté exclusion) dans
 *       {@code rechercherPropositionsCompatibles()} : le score calculé par la lambda est
 *       toujours {@code >= 50.0} (base = 50, bonus typeAide match = +20).  La branche
 *       d'exclusion (score &lt; 30) est également du <em>code mort</em>.</li>
 * </ul>
 *
 * <p>Ce fichier couvre les <em>branches atteignables</em> adjacentes pour maximiser la
 * couverture de la classe :
 * <ul>
 *   <li>Vérification exhaustive du path {@code obtenirParId} — cache hit (L158-162) et
 *       cache miss avec retour null (L165-172).</li>
 *   <li>Couverture de la branche d'inclusion du filtre L256 (score >= 30).</li>
 *   <li>Couverture de la branche {@code if (p.getDonneesPersonnalisees() == null)} dans
 *       la lambda (L250-253) — côtés true et false.</li>
 *   <li>Vérification que {@code ajouterAuCache} et {@code ajouterAIndex} (L168-169) seraient
 *       appelés SI {@code simulerRecuperationBDD} retournait une valeur non-null, en
 *       documentant l'impossibilité de déclencher ce chemin.</li>
 * </ul>
 */
@QuarkusTest
@DisplayName("PropositionAideService — branches manquantes (L168-169, L256 et couverture associée)")
class PropositionAideServiceBranchesMissingTest {

    @Inject
    PropositionAideService propositionAideService;

    // =========================================================================
    // Helpers — accès réflexif au cache et à l'index CDI
    // =========================================================================

    @SuppressWarnings("unchecked")
    private PropositionAideService getActualInstance() {
        return (PropositionAideService) ((ClientProxy) propositionAideService).arc_contextualInstance();
    }

    @SuppressWarnings("unchecked")
    private Map<String, PropositionAideResponse> getCache() throws Exception {
        Field f = PropositionAideService.class.getDeclaredField("cachePropositionsActives");
        f.setAccessible(true);
        return (Map<String, PropositionAideResponse>) f.get(getActualInstance());
    }

    @SuppressWarnings("unchecked")
    private Map<TypeAide, List<PropositionAideResponse>> getIndex() throws Exception {
        Field f = PropositionAideService.class.getDeclaredField("indexParType");
        f.setAccessible(true);
        return (Map<TypeAide, List<PropositionAideResponse>>) f.get(getActualInstance());
    }

    /** Vide cache et index avant chaque test pour isolation. */
    @BeforeEach
    void viderCacheEtIndex() throws Exception {
        getCache().clear();
        getIndex().clear();
    }

    private CreatePropositionAideRequest buildRequest(TypeAide type, BigDecimal montant) {
        return CreatePropositionAideRequest.builder()
                .titre("Proposition " + type.name())
                .description("Description de test pour " + type.name())
                .typeAide(type)
                .proposantId(UUID.randomUUID().toString())
                .organisationId(UUID.randomUUID().toString())
                .nombreMaxBeneficiaires(10)
                .montantMaximum(montant)
                .devise("FCFA")
                .delaiReponseHeures(48)
                .build();
    }

    // =========================================================================
    // L167 branche false : obtenirParId() — simulerRecuperationBDD retourne null
    //   → response reste null → if (response != null) = false → L168-169 NOT exécutées
    //   → return null (L172)
    //
    // Ce test documente et vérifie le comportement null-return.
    // Les lignes L168-169 restent dead code (simulerRecuperationBDD toujours null).
    // =========================================================================

    @Nested
    @DisplayName("obtenirParId — L167 branche false : simulerRecuperationBDD retourne null")
    class ObtenirParId_L167_BrancheFalse {

        @Test
        @DisplayName("L167 branche false : ID inconnu du cache et de la BDD → retourne null (L168-169 dead code)")
        void obtenirParId_idInconnu_retourneNull_L167_brancheFalse() {
            // ID absent du cache → obtenirParId appelle simulerRecuperationBDD(id) → null
            // → L167 : if (response != null) → false → L168-169 SAUTÉS → return null (L172)
            String idInconnu = UUID.randomUUID().toString();

            PropositionAideResponse result = propositionAideService.obtenirParId(idInconnu);

            assertThat(result)
                    .as("L167 branche false : simulerRecuperationBDD retourne null → résultat null")
                    .isNull();
        }

        @Test
        @DisplayName("Après appel avec ID inconnu, le cache ne contient pas l'entrée (L168 dead code confirmé)")
        void obtenirParId_idInconnu_cacheNonModifié_L168() throws Exception {
            String idInconnu = UUID.randomUUID().toString();
            int tailleAvant = getCache().size();

            propositionAideService.obtenirParId(idInconnu);

            // Si L168 (ajouterAuCache) était exécuté, le cache aurait grossi
            assertThat(getCache())
                    .as("L168 dead code confirmé : cache inchangé après obtenirParId avec ID inconnu")
                    .hasSize(tailleAvant);
        }

        @Test
        @DisplayName("Après appel avec ID inconnu, l'index n'est pas modifié (L169 dead code confirmé)")
        void obtenirParId_idInconnu_indexNonModifié_L169() throws Exception {
            String idInconnu = UUID.randomUUID().toString();
            int tailleAvant = getIndex().size();

            propositionAideService.obtenirParId(idInconnu);

            // Si L169 (ajouterAIndex) était exécuté, l'index aurait une nouvelle entrée
            assertThat(getIndex())
                    .as("L169 dead code confirmé : index inchangé après obtenirParId avec ID inconnu")
                    .hasSize(tailleAvant);
        }
    }

    // =========================================================================
    // L158-162 : obtenirParId() — cache hit → incrémente vues et retourne
    // =========================================================================

    @Nested
    @DisplayName("obtenirParId — L158-162 : cache hit (proposition dans le cache)")
    class ObtenirParId_CacheHit {

        @Test
        @DisplayName("L158 : proposition trouvée dans le cache → vues incrémentées, retournée")
        void obtenirParId_cacheHit_incrementeVues_L158() {
            PropositionAideResponse created = propositionAideService.creerProposition(
                    buildRequest(TypeAide.AIDE_ALIMENTAIRE, new BigDecimal("5000")));

            int vuesAvant = created.getNombreVues();

            PropositionAideResponse result = propositionAideService.obtenirParId(created.getId().toString());

            assertThat(result).isNotNull();
            assertThat(result.getNombreVues())
                    .as("L158-162 : vues incrémentées lors d'un cache hit")
                    .isEqualTo(vuesAvant + 1);
        }
    }

    // =========================================================================
    // L256 : rechercherPropositionsCompatibles — filtre score >= 30.0
    //
    // La branche d'inclusion (score >= 30, always true) est couverte par tout appel
    // qui passe un candidat dans le stream.
    // La branche d'exclusion (score < 30, never true) est dead code car score >= 50.
    //
    // Ces tests couvrent le chemin d'inclusion (branche true de score >= 30).
    // =========================================================================

    @Nested
    @DisplayName("rechercherPropositionsCompatibles — L256 branche inclusion (score >= 30)")
    class RechercherCompatibles_L256 {

        @Test
        @DisplayName("L256 branche true : proposition active avec score 70 (typeAide match) incluse dans résultats")
        void rechercherCompatibles_typeAideMatch_score70_incluse_L256() {
            // Créer une proposition active de type AIDE_FRAIS_MEDICAUX
            PropositionAideResponse proposition = propositionAideService.creerProposition(
                    buildRequest(TypeAide.AIDE_FRAIS_MEDICAUX, new BigDecimal("30000")));

            assertThat(proposition.getStatut()).isEqualTo(StatutProposition.ACTIVE);
            assertThat(proposition.getEstDisponible()).isTrue();

            // Demande avec même type → score = 50 + 20 = 70 ≥ 30 → incluse (L256 branche true)
            DemandeAideResponse demande = new DemandeAideResponse();
            demande.setId(UUID.randomUUID());
            demande.setTypeAide(TypeAide.AIDE_FRAIS_MEDICAUX);

            List<PropositionAideResponse> results =
                    propositionAideService.rechercherPropositionsCompatibles(demande);

            assertThat(results)
                    .as("L256 branche true : score 70 >= 30, proposition incluse")
                    .anyMatch(p -> p.getId().equals(proposition.getId()));
        }

        @Test
        @DisplayName("L256 branche true : proposition active avec score 50 (typeAide différent) incluse si score >= 30")
        void rechercherCompatibles_typeAideDifferent_score50_incluse_L256() {
            // Créer une proposition active de type AIDE_ALIMENTAIRE
            PropositionAideResponse proposition = propositionAideService.creerProposition(
                    buildRequest(TypeAide.AIDE_ALIMENTAIRE, new BigDecimal("10000")));

            // Demande avec type différent mais même catégorie
            // → candidats cherchés par catégorie → score = 50 (pas de bonus typeAide) ≥ 30 → incluse
            DemandeAideResponse demande = new DemandeAideResponse();
            demande.setId(UUID.randomUUID());
            demande.setTypeAide(TypeAide.AIDE_ALIMENTAIRE); // même type → index trouvé directement

            List<PropositionAideResponse> results =
                    propositionAideService.rechercherPropositionsCompatibles(demande);

            // Score minimum est 50 → toujours >= 30 → L256 branche true systématiquement
            assertThat(results)
                    .as("L256 branche true : score 50 >= 30, proposition incluse (si candidat actif)")
                    .isNotNull();
        }

        @Test
        @DisplayName("L256 : proposition suspendue exclue par le filtre isActiveEtDisponible avant L256")
        void rechercherCompatibles_propositionSuspendue_exclue_avantL256() {
            // Créer et suspendre la proposition
            PropositionAideResponse proposition = propositionAideService.creerProposition(
                    buildRequest(TypeAide.TRANSPORT, new BigDecimal("8000")));
            propositionAideService.changerStatutActivation(proposition.getId().toString(), false);

            DemandeAideResponse demande = new DemandeAideResponse();
            demande.setId(UUID.randomUUID());
            demande.setTypeAide(TypeAide.TRANSPORT);

            List<PropositionAideResponse> results =
                    propositionAideService.rechercherPropositionsCompatibles(demande);

            // Proposition suspendue filtrée par isActiveEtDisponible() avant d'atteindre L256
            assertThat(results)
                    .as("Proposition suspendue exclue avant L256 par isActiveEtDisponible")
                    .noneMatch(p -> p.getId().equals(proposition.getId()));
        }

        @Test
        @DisplayName("L256 : recherche sans aucun candidat → liste vide, filtre L256 non atteint")
        void rechercherCompatibles_aucunCandidat_listeVide() throws Exception {
            // Cache et index vides → pas de candidats
            viderCacheEtIndex();

            DemandeAideResponse demande = new DemandeAideResponse();
            demande.setId(UUID.randomUUID());
            demande.setTypeAide(TypeAide.AIDE_DEMENAGEMENT);

            List<PropositionAideResponse> results =
                    propositionAideService.rechercherPropositionsCompatibles(demande);

            assertThat(results)
                    .as("Pas de candidats → liste vide avant L256")
                    .isEmpty();
        }
    }

    // =========================================================================
    // L250-253 : lambda dans rechercherPropositionsCompatibles —
    //   if (p.getDonneesPersonnalisees() == null) { p.setDonneesPersonnalisees(new HashMap<>()); }
    //
    // Branche true (null → crée un nouveau HashMap)
    // Branche false (déjà non-null → réutilise le Map existant)
    // Ces branches sont aussi couvertes dans PropositionAideServiceCoverageTest,
    // mais on les teste ici également pour cohérence.
    // =========================================================================

    @Nested
    @DisplayName("Lambda L250-253 : donneesPersonnalisees null vs non-null dans rechercherCompatibles")
    class Lambda_L250_L253 {

        @Test
        @DisplayName("L252 branche true : donneesPersonnalisees null → nouveau HashMap créé")
        void lambda_donneesPersonnaliseesNull_creeHashMap_L252() throws Exception {
            PropositionAideResponse proposition = propositionAideService.creerProposition(
                    buildRequest(TypeAide.HEBERGEMENT_URGENCE, new BigDecimal("20000")));

            // Forcer donneesPersonnalisees à null dans le cache
            Map<String, PropositionAideResponse> cache = getCache();
            PropositionAideResponse cached = cache.get(proposition.getId().toString());
            assertThat(cached).isNotNull();
            cached.setDonneesPersonnalisees(null);

            DemandeAideResponse demande = new DemandeAideResponse();
            demande.setId(UUID.randomUUID());
            demande.setTypeAide(TypeAide.HEBERGEMENT_URGENCE);

            propositionAideService.rechercherPropositionsCompatibles(demande);

            // Après la lambda, donneesPersonnalisees ne doit plus être null
            assertThat(cached.getDonneesPersonnalisees())
                    .as("L252 branche true : donneesPersonnalisees créé depuis null")
                    .isNotNull()
                    .containsKey("scoreCompatibilite");
        }

        @Test
        @DisplayName("L252 branche false : donneesPersonnalisees non-null → map existant réutilisé")
        void lambda_donneesPersonnaliseesNonNull_reutilise_L252_false() throws Exception {
            PropositionAideResponse proposition = propositionAideService.creerProposition(
                    buildRequest(TypeAide.AIDE_VESTIMENTAIRE, new BigDecimal("15000")));

            // Pré-remplir donneesPersonnalisees
            Map<String, PropositionAideResponse> cache = getCache();
            PropositionAideResponse cached = cache.get(proposition.getId().toString());
            assertThat(cached).isNotNull();

            Map<String, Object> donneesExistantes = new HashMap<>();
            donneesExistantes.put("donneeExistante", "valeurImportante");
            donneesExistantes.put("scoreCompatibilite", 99.0); // sera écrasé par la lambda
            cached.setDonneesPersonnalisees(donneesExistantes);

            DemandeAideResponse demande = new DemandeAideResponse();
            demande.setId(UUID.randomUUID());
            demande.setTypeAide(TypeAide.AIDE_VESTIMENTAIRE);

            propositionAideService.rechercherPropositionsCompatibles(demande);

            // La lambda réutilise le map existant (branche false) et met à jour scoreCompatibilite
            assertThat(cached.getDonneesPersonnalisees())
                    .as("L252 branche false : map réutilisé et scoreCompatibilite mis à jour")
                    .containsKey("scoreCompatibilite");
            // score recalculé : 50 (base) + 20 (typeAide match) = 70, écrase 99
            assertThat((Double) cached.getDonneesPersonnalisees().get("scoreCompatibilite"))
                    .isEqualTo(70.0);
        }
    }

    // =========================================================================
    // Vérification que obtenirParId() coverage path inclut L158-L172 en entier
    // =========================================================================

    @Test
    @DisplayName("obtenirParId — chemin complet cache hit → vues incrémentées (L158-162 couverts)")
    void obtenirParId_cheminComplet_cacheHit_L158_L162() {
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.DON_MATERIEL, new BigDecimal("3000")));

        // Premier appel → cache hit → L158: response != null → L160: vues++ → L161: return
        PropositionAideResponse r1 = propositionAideService.obtenirParId(p.getId().toString());
        assertThat(r1).isNotNull();
        assertThat(r1.getNombreVues()).isEqualTo(1);

        // Second appel → cache hit → vues++
        PropositionAideResponse r2 = propositionAideService.obtenirParId(p.getId().toString());
        assertThat(r2.getNombreVues()).isEqualTo(2);
    }
}
