package dev.lions.unionflow.server.resource;

import dev.lions.unionflow.server.api.dto.souscription.FormuleAbonnementResponse;
import dev.lions.unionflow.server.api.dto.souscription.SouscriptionDemandeRequest;
import dev.lions.unionflow.server.api.dto.souscription.SouscriptionStatutResponse;
import dev.lions.unionflow.server.service.SouscriptionService;
import dev.lions.unionflow.server.service.support.SecuriteHelper;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * Tests unitaires purs (sans @QuarkusTest) pour SouscriptionResource.
 *
 * <p>Complète SouscriptionResourceMockTest (@QuarkusTest) en couvrant
 * directement les branches de logique interne via MockitoExtension,
 * notamment la branche null du body de rejeter() et d'autres chemins
 * non accessibles via RestAssured.
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("Tests SouscriptionResource (Mockito pur)")
class SouscriptionResourceTest {

    @Mock
    SouscriptionService souscriptionService;

    @Mock
    SecuriteHelper securiteHelper;

    @InjectMocks
    SouscriptionResource resource;

    private SouscriptionStatutResponse buildStatut(String statut) {
        SouscriptionStatutResponse r = new SouscriptionStatutResponse();
        r.setSouscriptionId(UUID.randomUUID().toString());
        r.setStatutValidation(statut);
        r.setStatutLibelle(statut);
        r.setMontantTotal(new BigDecimal("10000"));
        r.setOrganisationId(UUID.randomUUID().toString());
        r.setOrganisationNom("TestOrg");
        return r;
    }

    // ── GET /formules ─────────────────────────────────────────────────────────

    @Nested
    @DisplayName("GET /formules")
    class GetFormules {

        @Test
        @DisplayName("retourne 200 avec la liste des formules")
        void getFormules_listeNonVide_returns200() {
            FormuleAbonnementResponse f = new FormuleAbonnementResponse();
            f.setCode("BASIC");
            when(souscriptionService.getFormules()).thenReturn(List.of(f));

            Response r = resource.getFormules();

            assertThat(r.getStatus()).isEqualTo(200);
            @SuppressWarnings("unchecked")
            List<FormuleAbonnementResponse> body = (List<FormuleAbonnementResponse>) r.getEntity();
            assertThat(body).hasSize(1);
        }

        @Test
        @DisplayName("retourne 200 avec liste vide")
        void getFormules_listeVide_returns200() {
            when(souscriptionService.getFormules()).thenReturn(List.of());

            Response r = resource.getFormules();

            assertThat(r.getStatus()).isEqualTo(200);
        }

        @Test
        @DisplayName("appelle souscriptionService.getFormules() exactement une fois")
        void getFormules_appelleServiceUneFois() {
            when(souscriptionService.getFormules()).thenReturn(List.of());

            resource.getFormules();

            verify(souscriptionService, times(1)).getFormules();
            verifyNoInteractions(securiteHelper);
        }
    }

    // ── GET /ma-souscription ──────────────────────────────────────────────────

    @Nested
    @DisplayName("GET /ma-souscription")
    class GetMaSouscription {

        @Test
        @DisplayName("retourne 200 avec la souscription active")
        void getMaSouscription_success_returns200() {
            SouscriptionStatutResponse statut = buildStatut("ACTIVE");
            when(souscriptionService.getMaSouscription()).thenReturn(statut);

            Response r = resource.getMaSouscription();

            assertThat(r.getStatus()).isEqualTo(200);
            assertThat(r.getEntity()).isEqualTo(statut);
        }

        @Test
        @DisplayName("propage NotFoundException si aucune souscription")
        void getMaSouscription_aucuneSouscription_propagatesNotFound() {
            when(souscriptionService.getMaSouscription())
                    .thenThrow(new NotFoundException("Aucune souscription trouvée"));

            assertThatThrownBy(() -> resource.getMaSouscription())
                    .isInstanceOf(NotFoundException.class);
        }
    }

    // ── POST /demande ─────────────────────────────────────────────────────────

    @Nested
    @DisplayName("POST /demande")
    class CreerDemande {

        @Test
        @DisplayName("retourne 201 avec la réponse du service")
        void creerDemande_success_returns201() {
            SouscriptionDemandeRequest req = mock(SouscriptionDemandeRequest.class);
            SouscriptionStatutResponse statut = buildStatut("EN_ATTENTE_PAIEMENT");
            when(souscriptionService.creerDemande(req)).thenReturn(statut);

            Response r = resource.creerDemande(req);

            assertThat(r.getStatus()).isEqualTo(201);
            assertThat(r.getEntity()).isEqualTo(statut);
        }

        @Test
        @DisplayName("propage BadRequestException si souscription déjà existante")
        void creerDemande_dejaExistante_propagates() {
            SouscriptionDemandeRequest req = mock(SouscriptionDemandeRequest.class);
            when(souscriptionService.creerDemande(req))
                    .thenThrow(new BadRequestException("Souscription déjà existante"));

            assertThatThrownBy(() -> resource.creerDemande(req))
                    .isInstanceOf(BadRequestException.class);
        }

        @Test
        @DisplayName("propage NotFoundException si organisation inconnue")
        void creerDemande_orgInconnue_propagates() {
            SouscriptionDemandeRequest req = mock(SouscriptionDemandeRequest.class);
            when(souscriptionService.creerDemande(req))
                    .thenThrow(new NotFoundException("Organisation introuvable"));

            assertThatThrownBy(() -> resource.creerDemande(req))
                    .isInstanceOf(NotFoundException.class);
        }
    }

    // ── POST /{id}/initier-paiement ───────────────────────────────────────────

    @Nested
    @DisplayName("POST /{id}/initier-paiement")
    class InitierPaiement {

        @Test
        @DisplayName("retourne 200 avec waveLaunchUrl")
        void initierPaiement_success_returns200() {
            UUID id = UUID.randomUUID();
            SouscriptionStatutResponse statut = buildStatut("PAIEMENT_INITIE");
            statut.setWaveLaunchUrl("https://pay.wave.com/abc");
            when(souscriptionService.initierPaiementWave(id)).thenReturn(statut);

            Response r = resource.initierPaiement(id);

            assertThat(r.getStatus()).isEqualTo(200);
            assertThat(r.getEntity()).isEqualTo(statut);
        }

        @Test
        @DisplayName("propage NotFoundException si souscription introuvable")
        void initierPaiement_souscriptionInconnue_propagates() {
            UUID id = UUID.randomUUID();
            when(souscriptionService.initierPaiementWave(id))
                    .thenThrow(new NotFoundException("Souscription introuvable: " + id));

            assertThatThrownBy(() -> resource.initierPaiement(id))
                    .isInstanceOf(NotFoundException.class);
        }

        @Test
        @DisplayName("propage BadRequestException si statut invalide pour paiement")
        void initierPaiement_statutInvalide_propagates() {
            UUID id = UUID.randomUUID();
            when(souscriptionService.initierPaiementWave(id))
                    .thenThrow(new BadRequestException("Statut invalide pour initier paiement"));

            assertThatThrownBy(() -> resource.initierPaiement(id))
                    .isInstanceOf(BadRequestException.class);
        }
    }

    // ── POST /confirmer-paiement ──────────────────────────────────────────────

    @Nested
    @DisplayName("POST /confirmer-paiement")
    class ConfirmerPaiement {

        @Test
        @DisplayName("retourne 200 avec message de confirmation quand id et waveId fournis")
        void confirmerPaiement_success_returns200() {
            UUID id = UUID.randomUUID();
            doNothing().when(souscriptionService).confirmerPaiement(eq(id), eq("WAVE-123"));

            Response r = resource.confirmerPaiement(id, "WAVE-123");

            assertThat(r.getStatus()).isEqualTo(200);
            @SuppressWarnings("unchecked")
            Map<String, Object> body = (Map<String, Object>) r.getEntity();
            assertThat(body).containsEntry("message", "Paiement confirmé — compte activé");
        }

        @Test
        @DisplayName("retourne 200 quand waveId est null (optionnel)")
        void confirmerPaiement_waveIdNull_returns200() {
            UUID id = UUID.randomUUID();
            doNothing().when(souscriptionService).confirmerPaiement(eq(id), isNull());

            Response r = resource.confirmerPaiement(id, null);

            assertThat(r.getStatus()).isEqualTo(200);
        }

        @Test
        @DisplayName("lève BadRequestException quand id est null")
        void confirmerPaiement_idNull_throwsBadRequest() {
            assertThatThrownBy(() -> resource.confirmerPaiement(null, "WAVE-123"))
                    .isInstanceOf(BadRequestException.class)
                    .hasMessageContaining("'id' est obligatoire");
        }

        @Test
        @DisplayName("propage NotFoundException si souscription introuvable")
        void confirmerPaiement_souscriptionInconnue_propagates() {
            UUID id = UUID.randomUUID();
            doThrow(new NotFoundException("Souscription introuvable: " + id))
                    .when(souscriptionService).confirmerPaiement(eq(id), anyString());

            assertThatThrownBy(() -> resource.confirmerPaiement(id, "WAVE-TXN"))
                    .isInstanceOf(NotFoundException.class);
        }

        @Test
        @DisplayName("propage BadRequestException si service signale statut invalide")
        void confirmerPaiement_statutInvalide_propagates() {
            UUID id = UUID.randomUUID();
            doThrow(new BadRequestException("Impossible de confirmer depuis REJETEE"))
                    .when(souscriptionService).confirmerPaiement(eq(id), anyString());

            assertThatThrownBy(() -> resource.confirmerPaiement(id, "WAVE-BAD"))
                    .isInstanceOf(BadRequestException.class);
        }
    }

    // ── GET /admin/toutes ─────────────────────────────────────────────────────

    @Nested
    @DisplayName("GET /admin/toutes")
    class GetSouscriptionsToutes {

        @Test
        @DisplayName("retourne 200 avec la liste complète — sans filtre")
        void getSouscriptionsToutes_sansFiltreOrg_returns200() {
            when(souscriptionService.listerToutes(isNull(), eq(0), eq(1000)))
                    .thenReturn(List.of(buildStatut("ACTIVE"), buildStatut("EN_ATTENTE")));

            Response r = resource.getSouscriptionsToutes(null, 0, 1000);

            assertThat(r.getStatus()).isEqualTo(200);
            @SuppressWarnings("unchecked")
            List<SouscriptionStatutResponse> body = (List<SouscriptionStatutResponse>) r.getEntity();
            assertThat(body).hasSize(2);
        }

        @Test
        @DisplayName("retourne 200 avec filtre organisationId")
        void getSouscriptionsToutes_avecFiltreOrg_returns200() {
            UUID orgId = UUID.randomUUID();
            when(souscriptionService.listerToutes(eq(orgId), eq(0), eq(20)))
                    .thenReturn(List.of(buildStatut("ACTIVE")));

            Response r = resource.getSouscriptionsToutes(orgId, 0, 20);

            assertThat(r.getStatus()).isEqualTo(200);
        }

        @Test
        @DisplayName("retourne 200 avec liste vide")
        void getSouscriptionsToutes_listeVide_returns200() {
            when(souscriptionService.listerToutes(any(), anyInt(), anyInt())).thenReturn(List.of());

            Response r = resource.getSouscriptionsToutes(null, 0, 50);

            assertThat(r.getStatus()).isEqualTo(200);
        }
    }

    // ── GET /admin/organisation/{organisationId}/active ───────────────────────

    @Nested
    @DisplayName("GET /admin/organisation/{organisationId}/active")
    class GetActiveParOrganisation {

        @Test
        @DisplayName("retourne 200 quand souscription active trouvée")
        void getActiveParOrganisation_found_returns200() {
            UUID orgId = UUID.randomUUID();
            SouscriptionStatutResponse statut = buildStatut("ACTIVE");
            when(souscriptionService.obtenirActiveParOrganisation(orgId)).thenReturn(statut);

            Response r = resource.getActiveParOrganisation(orgId);

            assertThat(r.getStatus()).isEqualTo(200);
            assertThat(r.getEntity()).isEqualTo(statut);
        }

        @Test
        @DisplayName("retourne 404 quand service retourne null (aucune souscription active)")
        void getActiveParOrganisation_null_returns404() {
            UUID orgId = UUID.randomUUID();
            when(souscriptionService.obtenirActiveParOrganisation(orgId)).thenReturn(null);

            Response r = resource.getActiveParOrganisation(orgId);

            assertThat(r.getStatus()).isEqualTo(404);
            assertThat(r.getEntity()).isNull();
        }
    }

    // ── GET /admin/en-attente ─────────────────────────────────────────────────

    @Nested
    @DisplayName("GET /admin/en-attente")
    class GetSouscriptionsEnAttente {

        @Test
        @DisplayName("retourne 200 avec les souscriptions en attente")
        void getSouscriptionsEnAttente_avecEnAttente_returns200() {
            when(souscriptionService.getSouscriptionsEnAttenteValidation())
                    .thenReturn(List.of(buildStatut("PAIEMENT_CONFIRME")));

            Response r = resource.getSouscriptionsEnAttente();

            assertThat(r.getStatus()).isEqualTo(200);
            @SuppressWarnings("unchecked")
            List<SouscriptionStatutResponse> body = (List<SouscriptionStatutResponse>) r.getEntity();
            assertThat(body).hasSize(1);
        }

        @Test
        @DisplayName("retourne 200 avec liste vide si aucune en attente")
        void getSouscriptionsEnAttente_aucune_returns200() {
            when(souscriptionService.getSouscriptionsEnAttenteValidation()).thenReturn(List.of());

            Response r = resource.getSouscriptionsEnAttente();

            assertThat(r.getStatus()).isEqualTo(200);
        }
    }

    // ── POST /admin/{id}/approuver ────────────────────────────────────────────

    @Nested
    @DisplayName("POST /admin/{id}/approuver")
    class Approuver {

        @Test
        @DisplayName("retourne 200 avec message de confirmation après approbation réussie")
        void approuver_success_returns200() {
            UUID id = UUID.randomUUID();
            UUID superAdminId = UUID.randomUUID();
            when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
            doNothing().when(souscriptionService).approuver(id, superAdminId);

            Response r = resource.approuver(id);

            assertThat(r.getStatus()).isEqualTo(200);
            @SuppressWarnings("unchecked")
            Map<String, Object> body = (Map<String, Object>) r.getEntity();
            assertThat(body).containsEntry("message", "Souscription approuvée — compte activé");
        }

        @Test
        @DisplayName("appelle resolveMembreId pour récupérer l'identité du superAdmin")
        void approuver_appelleResolveMembreId() {
            UUID id = UUID.randomUUID();
            UUID superAdminId = UUID.randomUUID();
            when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);

            resource.approuver(id);

            verify(securiteHelper).resolveMembreId();
            verify(souscriptionService).approuver(id, superAdminId);
        }

        @Test
        @DisplayName("propage NotFoundException si souscription introuvable")
        void approuver_souscriptionInconnue_propagates() {
            UUID id = UUID.randomUUID();
            UUID superAdminId = UUID.randomUUID();
            when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
            doThrow(new NotFoundException("Souscription introuvable"))
                    .when(souscriptionService).approuver(id, superAdminId);

            assertThatThrownBy(() -> resource.approuver(id))
                    .isInstanceOf(NotFoundException.class);
        }

        @Test
        @DisplayName("propage BadRequestException si statut non approuvable")
        void approuver_statutNonApprouvable_propagates() {
            UUID id = UUID.randomUUID();
            UUID superAdminId = UUID.randomUUID();
            when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
            doThrow(new BadRequestException("Statut non approuvable"))
                    .when(souscriptionService).approuver(id, superAdminId);

            assertThatThrownBy(() -> resource.approuver(id))
                    .isInstanceOf(BadRequestException.class);
        }
    }

    // ── POST /admin/{id}/rejeter ──────────────────────────────────────────────

    @Nested
    @DisplayName("POST /admin/{id}/rejeter")
    class Rejeter {

        @Test
        @DisplayName("retourne 200 avec message de rejet quand commentaire présent")
        void rejeter_success_returns200() {
            UUID id = UUID.randomUUID();
            UUID superAdminId = UUID.randomUUID();
            when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
            doNothing().when(souscriptionService).rejeter(eq(id), eq(superAdminId), anyString());

            Response r = resource.rejeter(id, Map.of("commentaire", "Documents manquants"));

            assertThat(r.getStatus()).isEqualTo(200);
            @SuppressWarnings("unchecked")
            Map<String, Object> body = (Map<String, Object>) r.getEntity();
            assertThat(body).containsEntry("message", "Souscription rejetée");
        }

        @Test
        @DisplayName("lève BadRequestException quand commentaire absent (body null)")
        void rejeter_bodyNull_throwsBadRequest() {
            UUID id = UUID.randomUUID();

            assertThatThrownBy(() -> resource.rejeter(id, null))
                    .isInstanceOf(BadRequestException.class)
                    .hasMessageContaining("'commentaire' est obligatoire");
        }

        @Test
        @DisplayName("lève BadRequestException quand commentaire absent du map")
        void rejeter_commentaireAbsent_throwsBadRequest() {
            UUID id = UUID.randomUUID();

            assertThatThrownBy(() -> resource.rejeter(id, Map.of()))
                    .isInstanceOf(BadRequestException.class)
                    .hasMessageContaining("'commentaire' est obligatoire");
        }

        @Test
        @DisplayName("lève BadRequestException quand commentaire blank")
        void rejeter_commentaireBlank_throwsBadRequest() {
            UUID id = UUID.randomUUID();

            assertThatThrownBy(() -> resource.rejeter(id, Map.of("commentaire", "   ")))
                    .isInstanceOf(BadRequestException.class)
                    .hasMessageContaining("'commentaire' est obligatoire");
        }

        @Test
        @DisplayName("lève BadRequestException quand commentaire vide (string vide)")
        void rejeter_commentaireVide_throwsBadRequest() {
            UUID id = UUID.randomUUID();

            assertThatThrownBy(() -> resource.rejeter(id, Map.of("commentaire", "")))
                    .isInstanceOf(BadRequestException.class);
        }

        @Test
        @DisplayName("lève BadRequestException quand body contient commentaire null (Map.of ne supporte pas null values — utiliser HashMap)")
        void rejeter_commentaireNullDansMap_throwsBadRequest() {
            UUID id = UUID.randomUUID();
            Map<String, String> bodyAvecNull = new HashMap<>();
            bodyAvecNull.put("commentaire", null);

            assertThatThrownBy(() -> resource.rejeter(id, bodyAvecNull))
                    .isInstanceOf(BadRequestException.class)
                    .hasMessageContaining("'commentaire' est obligatoire");
        }

        @Test
        @DisplayName("appelle resolveMembreId et transmet le commentaire au service")
        void rejeter_passesCommentaireToService() {
            UUID id = UUID.randomUUID();
            UUID superAdminId = UUID.randomUUID();
            when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
            doNothing().when(souscriptionService).rejeter(any(), any(), any());

            resource.rejeter(id, Map.of("commentaire", "Motif valide"));

            verify(securiteHelper).resolveMembreId();
            verify(souscriptionService).rejeter(eq(id), eq(superAdminId), eq("Motif valide"));
        }

        @Test
        @DisplayName("propage NotFoundException si souscription introuvable")
        void rejeter_souscriptionInconnue_propagates() {
            UUID id = UUID.randomUUID();
            UUID superAdminId = UUID.randomUUID();
            when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
            doThrow(new NotFoundException("Souscription introuvable"))
                    .when(souscriptionService).rejeter(any(), any(), any());

            assertThatThrownBy(() -> resource.rejeter(id, Map.of("commentaire", "Motif")))
                    .isInstanceOf(NotFoundException.class);
        }

        @Test
        @DisplayName("propage BadRequestException si souscription déjà en état terminal")
        void rejeter_etatTerminal_propagates() {
            UUID id = UUID.randomUUID();
            UUID superAdminId = UUID.randomUUID();
            when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
            doThrow(new BadRequestException("État terminal"))
                    .when(souscriptionService).rejeter(any(), any(), any());

            assertThatThrownBy(() -> resource.rejeter(id, Map.of("commentaire", "Motif")))
                    .isInstanceOf(BadRequestException.class);
        }
    }
}
