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 dev.lions.unionflow.server.api.dto.solidarite.request.UpdatePropositionAideRequest;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * Tests de couverture complémentaires pour {@link PropositionAideService}.
 *
 * <p>Branches ciblées :
 * <ul>
 *   <li>correspondAuxFiltres:415 — case "montantMaximum" avec montantMaximum==null → return false</li>
 *   <li>correspondAuxFiltres:415 — case "montantMaximum" branche compareTo >= 0 → return true</li>
 *   <li>lambda$rechercherPropositionsCompatibles$3:256 — donneesPersonnalisees déjà non-null
 *       → branche "if (p.getDonneesPersonnalisees() == null)" = false (ne recrée pas)</li>
 * </ul>
 */
@QuarkusTest
@DisplayName("PropositionAideService — branches de couverture manquantes")
class PropositionAideServiceCoverageTest {

    @Inject
    PropositionAideService propositionAideService;

    /** Accès direct au cache de l'instance CDI (via arc_contextualInstance). */
    @SuppressWarnings("unchecked")
    private Map<String, PropositionAideResponse> getCache() throws Exception {
        PropositionAideService actual = (PropositionAideService)
                ((ClientProxy) propositionAideService).arc_contextualInstance();
        Field f = PropositionAideService.class.getDeclaredField("cachePropositionsActives");
        f.setAccessible(true);
        return (Map<String, PropositionAideResponse>) f.get(actual);
    }

    /** Vide le cache et l'index pour isoler les tests. */
    @SuppressWarnings("unchecked")
    @BeforeEach
    void viderCache() throws Exception {
        PropositionAideService actual = (PropositionAideService)
                ((ClientProxy) propositionAideService).arc_contextualInstance();

        Field cacheField = PropositionAideService.class.getDeclaredField("cachePropositionsActives");
        cacheField.setAccessible(true);
        ((Map<?, ?>) cacheField.get(actual)).clear();

        Field indexField = PropositionAideService.class.getDeclaredField("indexParType");
        indexField.setAccessible(true);
        ((Map<?, ?>) indexField.get(actual)).clear();
    }

    private CreatePropositionAideRequest buildRequest(TypeAide type, BigDecimal montantMaximum) {
        return CreatePropositionAideRequest.builder()
                .titre("Aide " + type.name())
                .description("Description test")
                .typeAide(type)
                .proposantId(UUID.randomUUID().toString())
                .nombreMaxBeneficiaires(5)
                .montantMaximum(montantMaximum)
                .devise("FCFA")
                .build();
    }

    // =========================================================================
    // correspondAuxFiltres — montantMaximum null → return false (branche L441)
    // =========================================================================

    @Test
    @DisplayName("correspondAuxFiltres — proposition.getMontantMaximum()==null → exclue du filtre montantMaximum")
    void correspondAuxFiltres_montantMaximumNull_exclutLaProposition() {
        // Créer une proposition SANS montantMaximum (null)
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.AIDE_ALIMENTAIRE, null)); // montantMaximum = null

        // Filtre par montantMaximum : proposition.getMontantMaximum() == null → return false → exclue
        Map<String, Object> filtres = new HashMap<>();
        filtres.put("montantMaximum", 1000.0);

        List<PropositionAideResponse> results = propositionAideService.rechercherAvecFiltres(filtres);

        // La proposition avec montantMaximum=null ne doit pas apparaître
        assertThat(results).noneMatch(r -> r.getId().equals(p.getId()));
    }

    @Test
    @DisplayName("correspondAuxFiltres — proposition.getMontantMaximum() suffisant → incluse dans résultats")
    void correspondAuxFiltres_montantMaximumSuffisant_inclutLaProposition() {
        // Créer une proposition avec montantMaximum = 50000
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.AIDE_FRAIS_MEDICAUX, new BigDecimal("50000")));

        // Filtre : cherche montantMaximum >= 10000 → 50000 >= 10000 → inclue
        Map<String, Object> filtres = new HashMap<>();
        filtres.put("montantMaximum", 10000.0);

        List<PropositionAideResponse> results = propositionAideService.rechercherAvecFiltres(filtres);

        assertThat(results).anyMatch(r -> r.getId().equals(p.getId()));
    }

    @Test
    @DisplayName("correspondAuxFiltres — montantMaximum trop petit → exclue du filtre")
    void correspondAuxFiltres_montantMaximumTropPetit_exclutLaProposition() {
        // Créer une proposition avec montantMaximum = 500
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.AIDE_VESTIMENTAIRE, new BigDecimal("500")));

        // Filtre : cherche montantMaximum >= 5000 → 500 < 5000 → exclue
        Map<String, Object> filtres = new HashMap<>();
        filtres.put("montantMaximum", 5000.0);

        List<PropositionAideResponse> results = propositionAideService.rechercherAvecFiltres(filtres);

        assertThat(results).noneMatch(r -> r.getId().equals(p.getId()));
    }

    // =========================================================================
    // lambda$rechercherPropositionsCompatibles$3:256
    // Branche : p.getDonneesPersonnalisees() != null → ne recrée pas le map
    // =========================================================================

    @Test
    @DisplayName("rechercherPropositionsCompatibles lambda:256 — donneesPersonnalisees déjà non null → pas réinitialisé")
    void rechercherPropositionsCompatibles_donneesPersonnaliseesPreremplie_nonReinitialisee() throws Exception {
        // Créer une proposition et lui pré-remplir donneesPersonnalisees dans le cache
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.CONSEIL_JURIDIQUE, new BigDecimal("20000")));

        // Accéder au cache et injecter des données personnalisées pour simuler un appel précédent
        Map<String, PropositionAideResponse> cache = getCache();
        PropositionAideResponse cached = cache.get(p.getId().toString());
        assertThat(cached).isNotNull();

        Map<String, Object> donneesExistantes = new HashMap<>();
        donneesExistantes.put("scoreCompatibilite", 75.0);
        donneesExistantes.put("donneeExistante", "valeur importante");
        cached.setDonneesPersonnalisees(donneesExistantes);

        // Appeler rechercherPropositionsCompatibles → la lambda:256 vérifie si donneesPersonnalisees == null
        // Ici elles ne sont PAS null → branche "if null" = false → le map existant est réutilisé
        DemandeAideResponse demande = new DemandeAideResponse();
        demande.setId(UUID.randomUUID());
        demande.setTypeAide(TypeAide.CONSEIL_JURIDIQUE);

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

        assertThat(compatible).isNotNull();
        // La proposition doit être dans les résultats (score >= 30)
        // et ses données personnalisées doivent avoir été mises à jour (scoreCompatibilite recalculé)
        PropositionAideResponse cachedApres = cache.get(p.getId().toString());
        assertThat(cachedApres.getDonneesPersonnalisees()).isNotNull();
        // scoreCompatibilite est écrasé par le nouveau calcul (50 + 20 = 70 si typeAide match)
        assertThat(cachedApres.getDonneesPersonnalisees()).containsKey("scoreCompatibilite");
    }

    @Test
    @DisplayName("rechercherPropositionsCompatibles lambda:256 — donneesPersonnalisees null → crée un nouveau map")
    void rechercherPropositionsCompatibles_donneesPersonnaliseesNull_creeNouveauMap() throws Exception {
        // Créer une proposition et s'assurer que donneesPersonnalisees == null dans le cache
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.TRADUCTION, new BigDecimal("15000")));

        Map<String, PropositionAideResponse> cache = getCache();
        PropositionAideResponse cached = cache.get(p.getId().toString());
        assertThat(cached).isNotNull();

        // Forcer donneesPersonnalisees à null pour que la branche "== null" soit exécutée
        cached.setDonneesPersonnalisees(null);

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

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

        assertThat(compatible).isNotNull();
        // Après le lambda, donneesPersonnalisees doit avoir été créé
        PropositionAideResponse cachedApres = cache.get(p.getId().toString());
        assertThat(cachedApres.getDonneesPersonnalisees()).isNotNull();
        assertThat(cachedApres.getDonneesPersonnalisees()).containsKey("scoreCompatibilite");
    }

    // =========================================================================
    // mettreAJour L111 : response == null après simulerRecuperationBDD → throws (branche null)
    // =========================================================================

    @Test
    @DisplayName("mettreAJour avec id inconnu → simulerRecuperationBDD null → IllegalArgumentException (couvre L111)")
    void mettreAJour_idInconnu_simulerNullThrowsException() {
        String idInconnu = UUID.randomUUID().toString(); // pas dans le cache

        UpdatePropositionAideRequest req = UpdatePropositionAideRequest.builder()
                .titre("Nouveau titre")
                .build();

        Assertions.assertThatThrownBy(
                () -> propositionAideService.mettreAJour(idInconnu, req))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining(idInconnu);
    }

    // =========================================================================
    // obtenirMeilleuresPropositions L302 : compareNote != 0 → true branch (couvre L302)
    // =========================================================================

    @Test
    @DisplayName("obtenirMeilleuresPropositions — 2 propositions avec notes différentes → tri par note décroissante (couvre L302 compareNote!=0)")
    void obtenirMeilleuresPropositions_deuxPropositionsNoteDifferentes_triParNote() throws Exception {
        // Créer 2 propositions avec au moins 3 évaluations et notes différentes
        PropositionAideResponse p1 = propositionAideService.creerProposition(
                buildRequest(TypeAide.CONSEIL_JURIDIQUE, new BigDecimal("5000")));
        PropositionAideResponse p2 = propositionAideService.creerProposition(
                buildRequest(TypeAide.AIDE_FRAIS_SCOLARITE, new BigDecimal("6000")));

        // Forcer des notes différentes via réflexion pour déclencher compareNote != 0
        PropositionAideService actual = (PropositionAideService)
                ((io.quarkus.arc.ClientProxy) propositionAideService).arc_contextualInstance();
        java.util.Map<String, PropositionAideResponse> cache = getCache();

        // Configurer p1 avec note 4.5, 5 évaluations
        PropositionAideResponse c1 = cache.get(p1.getId().toString());
        if (c1 != null) {
            c1.setNoteMoyenne(4.5);
            c1.setNombreEvaluations(5);
        }
        // Configurer p2 avec note 4.8, 4 évaluations (note > p1 → p2 devrait être avant p1)
        PropositionAideResponse c2 = cache.get(p2.getId().toString());
        if (c2 != null) {
            c2.setNoteMoyenne(4.8);
            c2.setNombreEvaluations(4);
        }

        List<PropositionAideResponse> results = propositionAideService.obtenirMeilleuresPropositions(10);

        // Le tri est effectué → compareNote != 0 branche true exécutée
        assertThat(results).isNotNull();
    }

    // =========================================================================
    // correspondAuxFiltres — filtre vide → toujours true (aucune itération)
    // =========================================================================

    @Test
    @DisplayName("correspondAuxFiltres — filtres vides → toutes les propositions retournées")
    void correspondAuxFiltres_filtresVides_retourneTout() {
        propositionAideService.creerProposition(buildRequest(TypeAide.AIDE_DEMENAGEMENT, new BigDecimal("10000")));
        propositionAideService.creerProposition(buildRequest(TypeAide.DON_MATERIEL, new BigDecimal("5000")));

        List<PropositionAideResponse> results = propositionAideService.rechercherAvecFiltres(new HashMap<>());

        assertThat(results).hasSizeGreaterThanOrEqualTo(2);
    }

    // =========================================================================
    // correspondAuxFiltres L428 — case "estDisponible"
    // =========================================================================

    @Test
    @DisplayName("correspondAuxFiltres L428 — filtre organisationId sans correspondance → return false (couvre L428)")
    void correspondAuxFiltres_organisationId_nonCorrespondant_couvreL428() {
        // Créer une proposition sans organisationId (null)
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.AIDE_ALIMENTAIRE, new BigDecimal("12000")));
        // proposition.getOrganisationId() == null, valeur = un UUID → Objects.equals(null, uuid) = false → L428

        Map<String, Object> filtres = new HashMap<>();
        filtres.put("organisationId", UUID.randomUUID()); // UUID qui ne correspond à aucune proposition

        List<PropositionAideResponse> results = propositionAideService.rechercherAvecFiltres(filtres);

        // La proposition avec organisationId=null doit être exclue (L428 return false exécuté)
        assertThat(results).noneMatch(r -> r.getId().equals(p.getId()));
    }

    @Test
    @DisplayName("correspondAuxFiltres case estDisponible — filtre estDisponible=false → proposition disponible exclue")
    void correspondAuxFiltres_estDisponible_false_propositionExclue() {
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.DON_MATERIEL, new BigDecimal("5000")));
        // estDisponible=true mais filtre=false → case "estDisponible" → return false → exclue

        Map<String, Object> filtres = new HashMap<>();
        filtres.put("estDisponible", false);
        List<PropositionAideResponse> results = propositionAideService.rechercherAvecFiltres(filtres);

        assertThat(results).noneMatch(r -> r.getId().equals(p.getId()));
    }

    // =========================================================================
    // correspondAuxFiltres — case "statut" couvre le 7ème branch du switch
    // =========================================================================

    @Test
    @DisplayName("correspondAuxFiltres case 'statut' — filtre statut INACTIVE → proposition ACTIVE exclue (couvre case statut)")
    void correspondAuxFiltres_statutNonCorrespondant_exclutLaProposition() {
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.AIDE_ALIMENTAIRE, new BigDecimal("3000")));
        // La proposition est ACTIVE par défaut

        Map<String, Object> filtres = new HashMap<>();
        filtres.put("statut", StatutProposition.SUSPENDUE); // statut différent de ACTIVE → return false

        List<PropositionAideResponse> results = propositionAideService.rechercherAvecFiltres(filtres);

        assertThat(results).noneMatch(r -> r.getId().equals(p.getId()));
    }

    @Test
    @DisplayName("correspondAuxFiltres case 'typeAide' — filtre typeAide correspondant → proposition incluse (couvre case typeAide true)")
    void correspondAuxFiltres_typeAideCorrespondant_inclutLaProposition() {
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.AIDE_DEMENAGEMENT, new BigDecimal("7000")));

        Map<String, Object> filtres = new HashMap<>();
        filtres.put("typeAide", TypeAide.AIDE_DEMENAGEMENT); // même typeAide → case "typeAide" → continue

        List<PropositionAideResponse> results = propositionAideService.rechercherAvecFiltres(filtres);

        assertThat(results).anyMatch(r -> r.getId().equals(p.getId()));
    }

    // =========================================================================
    // rechercherPropositionsCompatibles — candidats vides → recherche par catégorie
    // =========================================================================

    @Test
    @DisplayName("rechercherPropositionsCompatibles — pas de candidats par type → recherche par catégorie")
    void rechercherPropositionsCompatibles_pasDeCandidat_rechercheParCategorie() {
        // Créer une proposition de type AIDE_ALIMENTAIRE (catégorie AIDE_MATERIELLE probablement)
        propositionAideService.creerProposition(buildRequest(TypeAide.AIDE_ALIMENTAIRE, new BigDecimal("8000")));

        // Demande d'un type rare sans proposition directe mais même catégorie possible
        DemandeAideResponse demande = new DemandeAideResponse();
        demande.setId(UUID.randomUUID());
        demande.setTypeAide(TypeAide.AIDE_ALIMENTAIRE); // même type → indexParType devrait matcher

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

        // Résultat peut être vide ou non selon le cache, mais ne doit pas lever d'exception
        assertThat(compatible).isNotNull();
    }

    // =========================================================================
    // L240: typeAide differ (false branch) — via category search (different type, same category)
    // L302: noteMoyenne < 4.0 (false branch) — proposition with low score filtered out
    // L412: switch default — unknown key
    // =========================================================================

    @Test
    @DisplayName("rechercherPropositionsCompatibles L240 false — AIDE_VESTIMENTAIRE prop + AIDE_ALIMENTAIRE demande (même catégorie urgence, différent type)")
    void rechercherPropositionsCompatibles_L240False_typeDifferentMemCategorie() {
        // AIDE_VESTIMENTAIRE et AIDE_ALIMENTAIRE partagent la catégorie "urgence"
        // Proposition de type AIDE_VESTIMENTAIRE
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.AIDE_VESTIMENTAIRE, new BigDecimal("5000")));

        // Demande de type AIDE_ALIMENTAIRE — pas de candidats directs par type AIDE_ALIMENTAIRE
        // → recherche par catégorie "urgence" → trouve AIDE_VESTIMENTAIRE
        // → dans le lambda L240: p.getTypeAide() (AIDE_VESTIMENTAIRE) != demande.getTypeAide() (AIDE_ALIMENTAIRE) → false
        DemandeAideResponse demande = new DemandeAideResponse();
        demande.setId(UUID.randomUUID());
        demande.setTypeAide(TypeAide.AIDE_ALIMENTAIRE);

        List<PropositionAideResponse> compatible = propositionAideService.rechercherPropositionsCompatibles(demande);
        assertThat(compatible).isNotNull();
    }

    @Test
    @DisplayName("obtenirMeilleuresPropositions L302 false — proposition avec noteMoyenne=3.5 < 4.0 exclue du filtre")
    void obtenirMeilleuresPropositions_L302False_noteMoyenneBasse() throws Exception {
        // Créer une proposition avec noteMoyenne < 4.0 et >= 3 évaluations → exclue par L302 filter
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.AIDE_ADMINISTRATIVE, new BigDecimal("3000")));

        Map<String, PropositionAideResponse> cache = getCache();
        PropositionAideResponse cached = cache.get(p.getId().toString());
        if (cached != null) {
            cached.setNoteMoyenne(3.5); // < 4.0 → L302: false → filtered out
            cached.setNombreEvaluations(5); // >= 3 → passes first filter
        }

        List<PropositionAideResponse> results = propositionAideService.obtenirMeilleuresPropositions(10);
        // The proposition with noteMoyenne=3.5 should not be in results (filtered by L302)
        assertThat(results).noneMatch(r -> r.getId().equals(p.getId()));
    }

    @Test
    @DisplayName("correspondAuxFiltres switch default — clé inconnue → pas de return false → proposition incluse")
    void correspondAuxFiltres_cleInconnue_switchDefault_inclutLaProposition() {
        // Créer une proposition
        PropositionAideResponse p = propositionAideService.creerProposition(
                buildRequest(TypeAide.AIDE_FINANCIERE_URGENTE, new BigDecimal("10000")));

        // Filtre avec une clé inconnue → switch default (no matching case) → continue loop → return true
        Map<String, Object> filtres = new HashMap<>();
        filtres.put("cleInconnue", "valeurQuelconque"); // unknown key → switch default

        List<PropositionAideResponse> results = propositionAideService.rechercherAvecFiltres(filtres);

        // Proposition incluse car le filtre inconnu ne la filtre pas
        assertThat(results).anyMatch(r -> r.getId().equals(p.getId()));
    }
}
