package dev.lions.unionflow.server.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import dev.lions.unionflow.server.api.dto.finance.response.AdhesionResponse;
import dev.lions.unionflow.server.entity.DemandeAdhesion;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.AdhesionRepository;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/**
 * Tests unitaires purs pour {@link AdhesionService}.
 *
 * <p>Utilise Mockito directement (sans {@code @QuarkusTest}) pour pouvoir
 * contrôler précisément les dépendances et couvrir des branches difficiles
 * à atteindre via les tests d'intégration (contraintes DB, etc.).
 *
 * @author UnionFlow Team
 */
@DisplayName("AdhesionService - Tests unitaires")
class AdhesionServiceUnitTest {

    @Mock
    AdhesionRepository adhesionRepository;

    @Mock
    MembreRepository membreRepository;

    @Mock
    OrganisationRepository organisationRepository;

    @Mock
    MembreOrganisationRepository membreOrganisationRepository;

    @Mock
    MembreKeycloakSyncService keycloakSyncService;

    @Mock
    DefaultsService defaultsService;

    @Mock
    SecurityIdentity securityIdentity;

    @Mock
    JsonWebToken jwt;

    @InjectMocks
    AdhesionService adhesionService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
        // Par défaut, le principal n'est pas ADMIN_ORGANISATION → verifierAccesOrganisation retourne immédiatement
        when(securityIdentity.hasRole("ADMIN_ORGANISATION")).thenReturn(false);
    }

    // =========================================================================
    // approuverAdhesion — branche if (membre != null) → false (L156)
    // =========================================================================

    @Test
    @DisplayName("approuverAdhesion avec utilisateur null ignore le bloc activation membre et Keycloak (branche L156 false)")
    void approuverAdhesion_utilisateurNull_ignoreActivationMembre() {
        UUID id = UUID.randomUUID();

        // Adhésion sans utilisateur (utilisateur == null)
        DemandeAdhesion adhesion = new DemandeAdhesion();
        adhesion.setId(id);
        adhesion.setNumeroReference("ADH-UNIT-NULL");
        adhesion.setStatut("EN_ATTENTE");
        adhesion.setFraisAdhesion(BigDecimal.ONE);
        adhesion.setMontantPaye(BigDecimal.ZERO);
        adhesion.setCodeDevise("XOF");
        adhesion.setObservations("Test observation");
        adhesion.setDateDemande(LocalDateTime.now());
        // utilisateur intentionnellement null → L156 if (membre != null) → false

        when(adhesionRepository.findByIdOptional(id)).thenReturn(Optional.of(adhesion));

        // Appel de la méthode sous test
        AdhesionResponse result = adhesionService.approuverAdhesion(id, "AdminUnit");

        // Statut APPROUVEE atteint malgré utilisateur null
        assertThat(result.getStatut()).isEqualTo("APPROUVEE");
        assertThat(result.getObservations()).contains("AdminUnit");

        // provisionKeycloakUser ne doit JAMAIS être appelé (bloc membre null ignoré)
        verify(keycloakSyncService, never()).provisionKeycloakUser(any(UUID.class));
    }

    @Test
    @DisplayName("approuverAdhesion avec utilisateur non-null provisionne Keycloak (branche L156 true)")
    void approuverAdhesion_utilisateurNonNull_provisionneKeycloak() {
        UUID id = UUID.randomUUID();
        UUID membreId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setPrenom("Test");
        membre.setNom("Membre");
        membre.setEmail("test@test.com");
        membre.setStatutCompte("EN_ATTENTE_VALIDATION");
        membre.setActif(false);

        DemandeAdhesion adhesion = new DemandeAdhesion();
        adhesion.setId(id);
        adhesion.setNumeroReference("ADH-UNIT-MEMBRE");
        adhesion.setStatut("EN_ATTENTE");
        adhesion.setFraisAdhesion(BigDecimal.ONE);
        adhesion.setMontantPaye(BigDecimal.ZERO);
        adhesion.setCodeDevise("XOF");
        adhesion.setObservations("Test");
        adhesion.setDateDemande(LocalDateTime.now());
        adhesion.setUtilisateur(membre);

        when(adhesionRepository.findByIdOptional(id)).thenReturn(Optional.of(adhesion));

        AdhesionResponse result = adhesionService.approuverAdhesion(id, "AdminUnit");

        assertThat(result.getStatut()).isEqualTo("APPROUVEE");
        // provisionKeycloakUser DOIT être appelé (bloc membre non-null pris)
        verify(keycloakSyncService).provisionKeycloakUser(membreId);
    }

    @Test
    @DisplayName("approuverAdhesion : Keycloak provisionning échoue mais n'est pas bloquant (branche catch L162)")
    void approuverAdhesion_keycloakEchoue_nonBloquant() {
        UUID id = UUID.randomUUID();
        UUID membreId = UUID.randomUUID();

        Membre membre = new Membre();
        membre.setId(membreId);
        membre.setPrenom("Test");
        membre.setNom("Membre");
        membre.setEmail("test@test.com");
        membre.setStatutCompte("EN_ATTENTE_VALIDATION");
        membre.setActif(false);

        DemandeAdhesion adhesion = new DemandeAdhesion();
        adhesion.setId(id);
        adhesion.setNumeroReference("ADH-UNIT-KCLK");
        adhesion.setStatut("EN_ATTENTE");
        adhesion.setFraisAdhesion(BigDecimal.ONE);
        adhesion.setMontantPaye(BigDecimal.ZERO);
        adhesion.setCodeDevise("XOF");
        adhesion.setDateDemande(LocalDateTime.now());
        adhesion.setUtilisateur(membre);

        when(adhesionRepository.findByIdOptional(id)).thenReturn(Optional.of(adhesion));
        // provisionKeycloakUser lève exception → catch L162 → log warn + continue
        doThrow(new RuntimeException("Keycloak indisponible"))
                .when(keycloakSyncService).provisionKeycloakUser(membreId);

        // L'approbation doit réussir même si Keycloak échoue
        AdhesionResponse result = adhesionService.approuverAdhesion(id, "AdminUnit");

        assertThat(result.getStatut()).isEqualTo("APPROUVEE");
        assertThat(membre.getStatutCompte()).isEqualTo("ACTIF");
        assertThat(membre.getActif()).isTrue();
    }

    @Test
    @DisplayName("approuverAdhesion lève NotFoundException si adhésion introuvable (L141-143)")
    void approuverAdhesion_idInconnu_throwsNotFound() {
        UUID idInconnu = UUID.randomUUID();
        when(adhesionRepository.findByIdOptional(idInconnu)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> adhesionService.approuverAdhesion(idInconnu, "Admin"))
                .isInstanceOf(NotFoundException.class)
                .hasMessageContaining(idInconnu.toString());
    }

    @Test
    @DisplayName("approuverAdhesion lève IllegalStateException si adhésion non en attente (L145-147)")
    void approuverAdhesion_nonEnAttente_throwsIllegalState() {
        UUID id = UUID.randomUUID();

        DemandeAdhesion adhesion = new DemandeAdhesion();
        adhesion.setId(id);
        adhesion.setNumeroReference("ADH-UNIT-DEJA");
        adhesion.setStatut("APPROUVEE"); // Déjà approuvée → !isEnAttente() → true
        adhesion.setFraisAdhesion(BigDecimal.ONE);
        adhesion.setMontantPaye(BigDecimal.ZERO);
        adhesion.setCodeDevise("XOF");

        when(adhesionRepository.findByIdOptional(id)).thenReturn(Optional.of(adhesion));

        assertThatThrownBy(() -> adhesionService.approuverAdhesion(id, "Admin"))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("en attente");
    }

    // =========================================================================
    // rejeterAdhesion — branche if (membre != null) → false (L188)
    // =========================================================================

    @Test
    @DisplayName("rejeterAdhesion avec utilisateur null ignore le bloc désactivation membre (branche L188 false)")
    void rejeterAdhesion_utilisateurNull_ignoreDesactivationMembre() {
        UUID id = UUID.randomUUID();

        // Adhésion sans utilisateur (utilisateur == null)
        DemandeAdhesion adhesion = new DemandeAdhesion();
        adhesion.setId(id);
        adhesion.setNumeroReference("ADH-REJ-NULL");
        adhesion.setStatut("EN_ATTENTE");
        adhesion.setFraisAdhesion(BigDecimal.ONE);
        adhesion.setMontantPaye(BigDecimal.ZERO);
        adhesion.setCodeDevise("XOF");
        adhesion.setDateDemande(LocalDateTime.now());
        // utilisateur intentionnellement null → L188 if (membre != null) → false

        when(adhesionRepository.findByIdOptional(id)).thenReturn(Optional.of(adhesion));

        AdhesionResponse result = adhesionService.rejeterAdhesion(id, "Dossier incomplet");

        // Statut REJETEE atteint malgré utilisateur null
        assertThat(result.getStatut()).isEqualTo("REJETEE");
        assertThat(result.getMotifRejet()).isEqualTo("Dossier incomplet");
    }

    @Test
    @DisplayName("approuverAdhesion avec approuvePar null conserve les observations existantes (branche L152 null)")
    void approuverAdhesion_approuveParNull_conserveObservations() {
        UUID id = UUID.randomUUID();

        DemandeAdhesion adhesion = new DemandeAdhesion();
        adhesion.setId(id);
        adhesion.setNumeroReference("ADH-UNIT-APNULL");
        adhesion.setStatut("EN_ATTENTE");
        adhesion.setFraisAdhesion(BigDecimal.ONE);
        adhesion.setMontantPaye(BigDecimal.ZERO);
        adhesion.setCodeDevise("XOF");
        adhesion.setObservations("Observation conservée");
        adhesion.setDateDemande(LocalDateTime.now());
        // utilisateur null pour simplifier

        when(adhesionRepository.findByIdOptional(id)).thenReturn(Optional.of(adhesion));

        AdhesionResponse result = adhesionService.approuverAdhesion(id, null);

        assertThat(result.getStatut()).isEqualTo("APPROUVEE");
        // approuvePar == null → L152 prend adhesion.getObservations() (pas "Approuvée par : null")
        assertThat(result.getObservations()).isEqualTo("Observation conservée");
    }

    // =========================================================================
    // verifierAccesOrganisation — branches ADMIN_ORGANISATION (L300-323)
    // Ces branches ne sont PAS couvertes par les tests existants car setUp() configure
    // hasRole("ADMIN_ORGANISATION") = false → retour immédiat (L302).
    // =========================================================================

    @Test
    @DisplayName("verifierAccesOrganisation — ADMIN_ORGANISATION + adhesionOrgId null → ForbiddenException (L307)")
    void verifierAccesOrganisation_adminOrg_orgIdNull_leveForbiddenException() {
        UUID id = UUID.randomUUID();

        // Adhésion sans organisation → adhesionOrgId = null
        DemandeAdhesion adhesion = new DemandeAdhesion();
        adhesion.setId(id);
        adhesion.setNumeroReference("ADH-VERIF-NULL-ORG");
        adhesion.setStatut("EN_ATTENTE");
        adhesion.setFraisAdhesion(BigDecimal.ONE);
        adhesion.setMontantPaye(BigDecimal.ZERO);
        adhesion.setCodeDevise("XOF");
        adhesion.setDateDemande(java.time.LocalDateTime.now());
        adhesion.setOrganisation(null); // → adhesionOrgId = null → L307

        when(adhesionRepository.findByIdOptional(id)).thenReturn(Optional.of(adhesion));
        // Est ADMIN_ORGANISATION → ne retourne PAS immédiatement
        when(securityIdentity.hasRole("ADMIN_ORGANISATION")).thenReturn(true);

        assertThatThrownBy(() -> adhesionService.approuverAdhesion(id, "Admin"))
                .isInstanceOf(jakarta.ws.rs.ForbiddenException.class)
                .hasMessageContaining("organisation");
    }

    @Test
    @DisplayName("verifierAccesOrganisation — ADMIN_ORGANISATION + organisation non null + membre introuvable → ForbiddenException (L312 lambda)")
    void verifierAccesOrganisation_adminOrg_membreIntrouvable_leveForbiddenException() {
        UUID id = UUID.randomUUID();
        UUID orgId = UUID.randomUUID();

        dev.lions.unionflow.server.entity.Organisation org = new dev.lions.unionflow.server.entity.Organisation();
        org.setId(orgId);

        DemandeAdhesion adhesion = new DemandeAdhesion();
        adhesion.setId(id);
        adhesion.setNumeroReference("ADH-VERIF-MBRINT");
        adhesion.setStatut("EN_ATTENTE");
        adhesion.setFraisAdhesion(BigDecimal.ONE);
        adhesion.setMontantPaye(BigDecimal.ZERO);
        adhesion.setCodeDevise("XOF");
        adhesion.setDateDemande(java.time.LocalDateTime.now());
        adhesion.setOrganisation(org); // org non null

        when(adhesionRepository.findByIdOptional(id)).thenReturn(Optional.of(adhesion));
        when(securityIdentity.hasRole("ADMIN_ORGANISATION")).thenReturn(true);
        when(jwt.getSubject()).thenReturn("unknown-keycloak-subject");
        // Membre introuvable par keycloakUserId → L312 lambda → ForbiddenException
        when(membreRepository.findByKeycloakUserId("unknown-keycloak-subject")).thenReturn(Optional.empty());

        assertThatThrownBy(() -> adhesionService.approuverAdhesion(id, "Admin"))
                .isInstanceOf(jakarta.ws.rs.ForbiddenException.class)
                .hasMessageContaining("introuvable");
    }

    @Test
    @DisplayName("verifierAccesOrganisation — ADMIN_ORGANISATION + membre trouvé + n'appartient pas → ForbiddenException (L321)")
    void verifierAccesOrganisation_adminOrg_membreTrouveNAppartientPas_leveForbiddenException() {
        UUID id = UUID.randomUUID();
        UUID orgId = UUID.randomUUID();
        UUID membreId = UUID.randomUUID();

        dev.lions.unionflow.server.entity.Organisation org = new dev.lions.unionflow.server.entity.Organisation();
        org.setId(orgId);

        DemandeAdhesion adhesion = new DemandeAdhesion();
        adhesion.setId(id);
        adhesion.setNumeroReference("ADH-VERIF-NAPPART");
        adhesion.setStatut("EN_ATTENTE");
        adhesion.setFraisAdhesion(BigDecimal.ONE);
        adhesion.setMontantPaye(BigDecimal.ZERO);
        adhesion.setCodeDevise("XOF");
        adhesion.setDateDemande(java.time.LocalDateTime.now());
        adhesion.setOrganisation(org);

        Membre adminMembre = new Membre();
        adminMembre.setId(membreId);
        adminMembre.setEmail("admin.org@test.dev");

        when(adhesionRepository.findByIdOptional(id)).thenReturn(Optional.of(adhesion));
        when(securityIdentity.hasRole("ADMIN_ORGANISATION")).thenReturn(true);
        when(jwt.getSubject()).thenReturn("admin-keycloak-subject");
        when(membreRepository.findByKeycloakUserId("admin-keycloak-subject")).thenReturn(Optional.of(adminMembre));
        // Le membre n'appartient pas à cette organisation → L318 → ForbiddenException
        when(membreOrganisationRepository.findByMembreIdAndOrganisationId(membreId, orgId))
                .thenReturn(Optional.empty());

        assertThatThrownBy(() -> adhesionService.approuverAdhesion(id, "Admin"))
                .isInstanceOf(jakarta.ws.rs.ForbiddenException.class)
                .hasMessageContaining("organisation");
    }

    @Test
    @DisplayName("verifierAccesOrganisation — ADMIN_ORGANISATION + membre appartient → passe sans exception")
    void verifierAccesOrganisation_adminOrg_membreAppartient_passeSansException() {
        UUID id = UUID.randomUUID();
        UUID orgId = UUID.randomUUID();
        UUID membreId = UUID.randomUUID();

        dev.lions.unionflow.server.entity.Organisation org = new dev.lions.unionflow.server.entity.Organisation();
        org.setId(orgId);

        DemandeAdhesion adhesion = new DemandeAdhesion();
        adhesion.setId(id);
        adhesion.setNumeroReference("ADH-VERIF-OK");
        adhesion.setStatut("EN_ATTENTE");
        adhesion.setFraisAdhesion(BigDecimal.ONE);
        adhesion.setMontantPaye(BigDecimal.ZERO);
        adhesion.setCodeDevise("XOF");
        adhesion.setDateDemande(java.time.LocalDateTime.now());
        adhesion.setOrganisation(org);

        Membre adminMembre = new Membre();
        adminMembre.setId(membreId);
        adminMembre.setEmail("admin.ok@test.dev");

        dev.lions.unionflow.server.entity.MembreOrganisation mo =
                new dev.lions.unionflow.server.entity.MembreOrganisation();
        mo.setId(UUID.randomUUID());

        when(adhesionRepository.findByIdOptional(id)).thenReturn(Optional.of(adhesion));
        when(securityIdentity.hasRole("ADMIN_ORGANISATION")).thenReturn(true);
        when(jwt.getSubject()).thenReturn("admin-ok-subject");
        when(membreRepository.findByKeycloakUserId("admin-ok-subject")).thenReturn(Optional.of(adminMembre));
        // Membre appartient à l'organisation → pas de ForbiddenException
        when(membreOrganisationRepository.findByMembreIdAndOrganisationId(membreId, orgId))
                .thenReturn(Optional.of(mo));

        // approuverAdhesion doit réussir (L301 → vérif passe)
        AdhesionResponse result = adhesionService.approuverAdhesion(id, "AdminOrg");

        assertThat(result.getStatut()).isEqualTo("APPROUVEE");
    }
}
