package dev.lions.unionflow.server.resource;

import dev.lions.unionflow.server.api.payment.*;
import dev.lions.unionflow.server.entity.FormuleAbonnement;
import dev.lions.unionflow.server.entity.SouscriptionOrganisation;
import dev.lions.unionflow.server.payment.orchestration.PaymentOrchestrator;
import dev.lions.unionflow.server.payment.orchestration.PaymentProviderRegistry;
import dev.lions.unionflow.server.repository.SouscriptionOrganisationRepository;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.Response;
import org.junit.jupiter.api.BeforeEach;
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.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

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

@ExtendWith(MockitoExtension.class)
@DisplayName("Tests PaiementUnifieResource")
class PaiementUnifieResourceTest {

    @Mock
    PaymentOrchestrator orchestrator;

    @Mock
    PaymentProviderRegistry registry;

    @Mock
    SouscriptionOrganisationRepository souscriptionRepository;

    @InjectMocks
    PaiementUnifieResource resource;

    private CheckoutSession buildSession() {
        return new CheckoutSession("ext-123", "https://pay.example.com/checkout", Instant.now(), Map.of());
    }

    private PaiementUnifieResource.PaiementInitierRequest buildRequest(UUID souscriptionId) {
        return new PaiementUnifieResource.PaiementInitierRequest(
                new BigDecimal("5000"),
                "XOF",
                "+22178000001",
                "test@example.com",
                "REF-001",
                "https://success.example.com",
                "https://cancel.example.com",
                souscriptionId
        );
    }

    // ── POST /api/paiements/initier ───────────────────────────────────────────

    @Nested
    @DisplayName("POST /initier")
    class Initier {

        @Test
        @DisplayName("retourne 200 quand paiement initié avec succès — provider explicite")
        void initier_avecProviderExplicite_returns200() throws Exception {
            PaiementUnifieResource.PaiementInitierRequest req = buildRequest(null);
            CheckoutSession session = buildSession();
            when(orchestrator.initierPaiement(any(CheckoutRequest.class), eq("WAVE"))).thenReturn(session);

            Response r = resource.initier("WAVE", req);

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

        @Test
        @DisplayName("retourne 200 quand paiement initié — provider null (provider par défaut)")
        void initier_sansProv_returns200() throws Exception {
            PaiementUnifieResource.PaiementInitierRequest req = buildRequest(null);
            CheckoutSession session = buildSession();
            when(orchestrator.initierPaiement(any(CheckoutRequest.class), isNull())).thenReturn(session);

            Response r = resource.initier(null, req);

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

        @Test
        @DisplayName("retourne 200 quand devise null — XOF utilisé par défaut")
        void initier_deviseNull_utilisesXOF_returns200() throws Exception {
            PaiementUnifieResource.PaiementInitierRequest req = new PaiementUnifieResource.PaiementInitierRequest(
                    new BigDecimal("3000"), null, "+22178000001", "test@example.com",
                    "REF-002", null, null, null
            );
            CheckoutSession session = buildSession();
            when(orchestrator.initierPaiement(
                    argThat(c -> "XOF".equals(c.currency())), isNull()
            )).thenReturn(session);

            Response r = resource.initier(null, req);

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

        @Test
        @DisplayName("retourne 200 — provider résolu depuis formule quand souscriptionId fourni")
        void initier_avecSouscriptionId_providerDepuisFormule_returns200() throws Exception {
            UUID souscriptionId = UUID.randomUUID();
            PaiementUnifieResource.PaiementInitierRequest req = buildRequest(souscriptionId);

            FormuleAbonnement formule = mock(FormuleAbonnement.class);
            when(formule.getProviderDefaut()).thenReturn("ORANGE_MONEY");

            SouscriptionOrganisation souscription = mock(SouscriptionOrganisation.class);
            when(souscription.getFormule()).thenReturn(formule);
            when(souscriptionRepository.findByIdOptional(souscriptionId)).thenReturn(Optional.of(souscription));

            CheckoutSession session = buildSession();
            when(orchestrator.initierPaiement(any(CheckoutRequest.class), eq("ORANGE_MONEY"))).thenReturn(session);

            Response r = resource.initier("WAVE", req);

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

        @Test
        @DisplayName("retourne 200 — fallback sur provider requête si formule sans providerDefaut")
        void initier_formuleSansProvider_fallbackQueryParam() throws Exception {
            UUID souscriptionId = UUID.randomUUID();
            PaiementUnifieResource.PaiementInitierRequest req = buildRequest(souscriptionId);

            FormuleAbonnement formule = mock(FormuleAbonnement.class);
            when(formule.getProviderDefaut()).thenReturn(null);

            SouscriptionOrganisation souscription = mock(SouscriptionOrganisation.class);
            when(souscription.getFormule()).thenReturn(formule);
            when(souscriptionRepository.findByIdOptional(souscriptionId)).thenReturn(Optional.of(souscription));

            CheckoutSession session = buildSession();
            when(orchestrator.initierPaiement(any(CheckoutRequest.class), eq("WAVE"))).thenReturn(session);

            Response r = resource.initier("WAVE", req);

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

        @Test
        @DisplayName("retourne 200 — souscription introuvable, fallback provider explicite")
        void initier_souscriptionAbsente_fallbackProvider() throws Exception {
            UUID souscriptionId = UUID.randomUUID();
            PaiementUnifieResource.PaiementInitierRequest req = buildRequest(souscriptionId);

            when(souscriptionRepository.findByIdOptional(souscriptionId)).thenReturn(Optional.empty());

            CheckoutSession session = buildSession();
            when(orchestrator.initierPaiement(any(CheckoutRequest.class), eq("WAVE"))).thenReturn(session);

            Response r = resource.initier("WAVE", req);

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

        @Test
        @DisplayName("retourne 200 — providerDefaut blank, fallback sur query param")
        void initier_providerDefautBlank_fallback() throws Exception {
            UUID souscriptionId = UUID.randomUUID();
            PaiementUnifieResource.PaiementInitierRequest req = buildRequest(souscriptionId);

            FormuleAbonnement formule = mock(FormuleAbonnement.class);
            when(formule.getProviderDefaut()).thenReturn("   ");

            SouscriptionOrganisation souscription = mock(SouscriptionOrganisation.class);
            when(souscription.getFormule()).thenReturn(formule);
            when(souscriptionRepository.findByIdOptional(souscriptionId)).thenReturn(Optional.of(souscription));

            CheckoutSession session = buildSession();
            when(orchestrator.initierPaiement(any(CheckoutRequest.class), eq("WAVE"))).thenReturn(session);

            Response r = resource.initier("WAVE", req);

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

        @Test
        @DisplayName("retourne statut HTTP de PaymentException quand provider échoue")
        void initier_paymentException_returnsProviderStatus() throws Exception {
            PaiementUnifieResource.PaiementInitierRequest req = buildRequest(null);
            when(orchestrator.initierPaiement(any(), any()))
                    .thenThrow(new PaymentException("WAVE", "Compte insuffisant", 402));

            Response r = resource.initier("WAVE", req);

            assertThat(r.getStatus()).isEqualTo(402);
            @SuppressWarnings("unchecked")
            Map<String, Object> body = (Map<String, Object>) r.getEntity();
            assertThat(body).containsKey("error");
            assertThat(body).containsEntry("provider", "WAVE");
        }

        @Test
        @DisplayName("retourne 503 quand aucun provider disponible")
        void initier_aucunProvider_returns503() throws Exception {
            PaiementUnifieResource.PaiementInitierRequest req = buildRequest(null);
            when(orchestrator.initierPaiement(any(), any()))
                    .thenThrow(new PaymentException("NONE", "Aucun provider disponible", 503));

            Response r = resource.initier(null, req);

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

    // ── POST /api/paiements/webhook/{provider} ────────────────────────────────

    @Nested
    @DisplayName("POST /webhook/{provider}")
    class Webhook {

        private HttpHeaders buildHeaders(Map<String, List<String>> headerMap) {
            HttpHeaders headers = mock(HttpHeaders.class);
            MultivaluedHashMap<String, String> multivalued = new MultivaluedHashMap<>();
            headerMap.forEach(multivalued::put);
            when(headers.getRequestHeaders()).thenReturn(multivalued);
            return headers;
        }

        @Test
        @DisplayName("retourne 200 quand webhook traité avec succès")
        void webhook_success_returns200() throws Exception {
            PaymentProvider provider = mock(PaymentProvider.class);
            PaymentEvent event = new PaymentEvent("ext-1", "REF-001", PaymentStatus.SUCCESS,
                    new BigDecimal("5000"), "TXN-001", Instant.now());

            when(registry.get("WAVE")).thenReturn(provider);
            when(provider.processWebhook(anyString(), anyMap())).thenReturn(event);
            doNothing().when(orchestrator).handleEvent(event);

            HttpHeaders headers = buildHeaders(Map.of("X-Wave-Signature", List.of("sha256=abc")));
            Response r = resource.webhook("WAVE", "{}", headers);

            assertThat(r.getStatus()).isEqualTo(200);
            verify(orchestrator).handleEvent(event);
        }

        @Test
        @DisplayName("retourne 200 quand headers vides")
        void webhook_headersVides_returns200() throws Exception {
            PaymentProvider provider = mock(PaymentProvider.class);
            PaymentEvent event = new PaymentEvent("ext-2", "REF-002", PaymentStatus.SUCCESS,
                    new BigDecimal("1000"), "TXN-002", Instant.now());

            when(registry.get("ORANGE")).thenReturn(provider);
            when(provider.processWebhook(anyString(), anyMap())).thenReturn(event);
            doNothing().when(orchestrator).handleEvent(event);

            HttpHeaders headers = buildHeaders(Map.of());
            Response r = resource.webhook("ORANGE", "body", headers);

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

        @Test
        @DisplayName("retourne 200 quand header avec liste non vide")
        void webhook_headerListeNonVide_returns200() throws Exception {
            PaymentProvider provider = mock(PaymentProvider.class);
            PaymentEvent event = new PaymentEvent("ext-3", "REF-003", PaymentStatus.SUCCESS,
                    new BigDecimal("2000"), "TXN-003", Instant.now());

            when(registry.get("MTN")).thenReturn(provider);
            when(provider.processWebhook(anyString(), anyMap())).thenReturn(event);
            doNothing().when(orchestrator).handleEvent(event);

            HttpHeaders headers = buildHeaders(Map.of("Authorization", Arrays.asList("Bearer token123")));
            Response r = resource.webhook("MTN", "rawbody", headers);

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

        @Test
        @DisplayName("retourne 404 quand provider inconnu (UnsupportedOperationException)")
        void webhook_providerInconnu_returns404() throws Exception {
            when(registry.get("UNKNOWN")).thenThrow(new UnsupportedOperationException("Provider inconnu"));

            HttpHeaders headers = buildHeaders(Map.of());
            Response r = resource.webhook("UNKNOWN", "{}", headers);

            assertThat(r.getStatus()).isEqualTo(404);
            @SuppressWarnings("unchecked")
            Map<String, Object> body = (Map<String, Object>) r.getEntity();
            assertThat(body).containsKey("error");
        }

        @Test
        @DisplayName("retourne statut HTTP de PaymentException quand signature invalide")
        void webhook_signatureInvalide_returnsProviderStatus() throws Exception {
            PaymentProvider provider = mock(PaymentProvider.class);
            when(registry.get("WAVE")).thenReturn(provider);
            when(provider.processWebhook(anyString(), anyMap()))
                    .thenThrow(new PaymentException("WAVE", "Signature invalide", 400));

            HttpHeaders headers = buildHeaders(Map.of());
            Response r = resource.webhook("WAVE", "tampered", headers);

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

        @Test
        @DisplayName("retourne 401 quand PaymentException avec statut 401")
        void webhook_paymentException401_returns401() throws Exception {
            PaymentProvider provider = mock(PaymentProvider.class);
            when(registry.get("WAVE")).thenReturn(provider);
            when(provider.processWebhook(anyString(), anyMap()))
                    .thenThrow(new PaymentException("WAVE", "Non autorisé", 401));

            HttpHeaders headers = buildHeaders(Map.of());
            Response r = resource.webhook("WAVE", "body", headers);

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

        @Test
        @DisplayName("provider code converti en majuscules")
        void webhook_providerCodeLowercase_convertedToUppercase() throws Exception {
            PaymentProvider provider = mock(PaymentProvider.class);
            PaymentEvent event = new PaymentEvent("ext-4", "REF-004", PaymentStatus.SUCCESS,
                    new BigDecimal("500"), "TXN-004", Instant.now());

            when(registry.get("WAVE")).thenReturn(provider);
            when(provider.processWebhook(anyString(), anyMap())).thenReturn(event);
            doNothing().when(orchestrator).handleEvent(event);

            HttpHeaders headers = buildHeaders(Map.of());
            Response r = resource.webhook("wave", "{}", headers);

            assertThat(r.getStatus()).isEqualTo(200);
            verify(registry).get("WAVE");
        }
    }

    // ── GET /api/paiements/providers ─────────────────────────────────────────

    @Nested
    @DisplayName("GET /providers")
    class GetProviders {

        @Test
        @DisplayName("retourne la liste des providers disponibles")
        void getProviders_retourneListeProviders() {
            when(registry.getAvailableCodes()).thenReturn(List.of("WAVE", "ORANGE_MONEY", "MTN_MOMO"));

            List<String> result = resource.getProviders();

            assertThat(result).containsExactly("WAVE", "ORANGE_MONEY", "MTN_MOMO");
        }

        @Test
        @DisplayName("retourne liste vide quand aucun provider enregistré")
        void getProviders_aucunProvider_listeVide() {
            when(registry.getAvailableCodes()).thenReturn(List.of());

            List<String> result = resource.getProviders();

            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("retourne liste avec un seul provider")
        void getProviders_unSeulProvider() {
            when(registry.getAvailableCodes()).thenReturn(List.of("WAVE"));

            List<String> result = resource.getProviders();

            assertThat(result).hasSize(1).containsExactly("WAVE");
        }
    }
}
