package dev.lions.unionflow.server.payment.wave;

import dev.lions.unionflow.server.api.payment.CheckoutRequest;
import dev.lions.unionflow.server.api.payment.CheckoutSession;
import dev.lions.unionflow.server.api.payment.PaymentEvent;
import dev.lions.unionflow.server.api.payment.PaymentException;
import dev.lions.unionflow.server.api.payment.PaymentStatus;
import dev.lions.unionflow.server.service.WaveCheckoutService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.HexFormat;
import java.util.Map;

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.when;

@ExtendWith(MockitoExtension.class)
class WavePaymentProviderTest {

    @Mock
    WaveCheckoutService waveCheckoutService;

    private WavePaymentProvider provider;

    @BeforeEach
    void setUp() throws Exception {
        provider = new WavePaymentProvider();
        Field serviceField = WavePaymentProvider.class.getDeclaredField("waveCheckoutService");
        serviceField.setAccessible(true);
        serviceField.set(provider, waveCheckoutService);
        // webhookSecret defaults to ""
    }

    @Test
    void getProviderCode_returns_WAVE() {
        assertThat(provider.getProviderCode()).isEqualTo("WAVE");
    }

    @Test
    void code_constant_is_WAVE() {
        assertThat(WavePaymentProvider.CODE).isEqualTo("WAVE");
    }

    @Test
    void isAvailable_returnsTrue() {
        // WavePaymentProvider uses the default implementation which always returns true
        assertThat(provider.isAvailable()).isTrue();
    }

    @Test
    void getStatus_returnsProcessing() throws Exception {
        PaymentStatus status = provider.getStatus("WAVE-EXT-123");
        assertThat(status).isEqualTo(PaymentStatus.PROCESSING);
    }

    @Test
    void initiateCheckout_delegatesToWaveCheckoutService() throws Exception {
        WaveCheckoutService.WaveCheckoutSessionResponse mockResp =
                new WaveCheckoutService.WaveCheckoutSessionResponse("WAVE-SESSION-001", "https://pay.wave.com/c/WAVE-SESSION-001");

        when(waveCheckoutService.createSession(any(), any(), any(), any(), any(), any()))
                .thenReturn(mockResp);

        CheckoutRequest req = new CheckoutRequest(
                BigDecimal.valueOf(5000), "XOF",
                "+2210100000000", "test@wave.com",
                "REF-WAVE-001", "http://success", "http://cancel", Map.of());

        CheckoutSession session = provider.initiateCheckout(req);

        assertThat(session.externalId()).isEqualTo("WAVE-SESSION-001");
        assertThat(session.checkoutUrl()).isEqualTo("https://pay.wave.com/c/WAVE-SESSION-001");
        assertThat(session.expiresAt()).isNotNull();
        assertThat(session.providerMetadata()).containsEntry("provider", "WAVE");
    }

    @Test
    void initiateCheckout_whenServiceThrows_wrapsInPaymentException() throws Exception {
        when(waveCheckoutService.createSession(any(), any(), any(), any(), any(), any()))
                .thenThrow(new RuntimeException("Wave API error"));

        CheckoutRequest req = new CheckoutRequest(
                BigDecimal.valueOf(1000), "XOF",
                "+2210100000001", "fail@wave.com",
                "REF-FAIL", "http://success", "http://cancel", Map.of());

        assertThatThrownBy(() -> provider.initiateCheckout(req))
                .isInstanceOf(PaymentException.class)
                .hasMessageContaining("WAVE");
    }

    @Test
    void processWebhook_whenNoSecret_parsesCompletedEvent() throws Exception {
        String json = "{\"type\":\"checkout.session.completed\",\"data\":{\"id\":\"WAVE-EXT-001\",\"client_reference\":\"REF-001\",\"amount\":\"5000\",\"transaction_id\":\"TXN-001\"}}";

        PaymentEvent event = provider.processWebhook(json, Map.of());

        assertThat(event.externalId()).isEqualTo("WAVE-EXT-001");
        assertThat(event.reference()).isEqualTo("REF-001");
        assertThat(event.status()).isEqualTo(PaymentStatus.SUCCESS);
        assertThat(event.amountConfirmed()).isEqualByComparingTo(new BigDecimal("5000"));
        assertThat(event.transactionCode()).isEqualTo("TXN-001");
    }

    @Test
    void processWebhook_failedEvent_returnsFailed() throws Exception {
        String json = "{\"type\":\"checkout.session.failed\",\"data\":{\"id\":\"WAVE-EXT-002\",\"client_reference\":\"REF-002\",\"amount\":\"1000\"}}";

        PaymentEvent event = provider.processWebhook(json, Map.of());

        assertThat(event.status()).isEqualTo(PaymentStatus.FAILED);
    }

    @Test
    void processWebhook_expiredEvent_returnsExpired() throws Exception {
        String json = "{\"type\":\"checkout.session.expired\",\"data\":{\"id\":\"WAVE-EXT-003\",\"client_reference\":\"REF-003\",\"amount\":\"2000\"}}";

        PaymentEvent event = provider.processWebhook(json, Map.of());

        assertThat(event.status()).isEqualTo(PaymentStatus.EXPIRED);
    }

    @Test
    void processWebhook_unknownEvent_returnsProcessing() throws Exception {
        String json = "{\"type\":\"some.unknown.event\",\"data\":{\"id\":\"WAVE-EXT-004\",\"client_reference\":\"REF-004\",\"amount\":\"500\"}}";

        PaymentEvent event = provider.processWebhook(json, Map.of());

        assertThat(event.status()).isEqualTo(PaymentStatus.PROCESSING);
    }

    @Test
    void processWebhook_whenInvalidJson_throwsPaymentException() {
        String notJson = "not-valid-json{{{";

        assertThatThrownBy(() -> provider.processWebhook(notJson, Map.of()))
                .isInstanceOf(PaymentException.class)
                .hasMessageContaining("Wave");
    }

    @Test
    void processWebhook_whenSignaturePresentButNoSecret_skipsVerification() throws Exception {
        // webhookSecret is empty => no verification
        String json = "{\"type\":\"checkout.session.completed\",\"data\":{\"id\":\"W-EXT-005\",\"client_reference\":\"REF-005\",\"amount\":\"100\"}}";

        Map<String, String> headers = Map.of("wave-signature", "t=1234,v1=irrelevant");

        PaymentEvent event = provider.processWebhook(json, headers);
        assertThat(event.status()).isEqualTo(PaymentStatus.SUCCESS);
    }

    @Test
    void processWebhook_whenSignatureInvalid_throwsPaymentException() throws Exception {
        Field secretField = WavePaymentProvider.class.getDeclaredField("webhookSecret");
        secretField.setAccessible(true);
        secretField.set(provider, "secret123");

        String json = "{\"type\":\"checkout.session.completed\",\"data\":{\"id\":\"W-EXT-006\",\"client_reference\":\"REF-006\",\"amount\":\"500\"}}";
        Map<String, String> headers = Map.of("wave-signature", "t=1234,v1=invalidsignature");

        assertThatThrownBy(() -> provider.processWebhook(json, headers))
                .isInstanceOf(PaymentException.class)
                .hasMessageContaining("invalide");
    }

    @Test
    void processWebhook_whenSignatureValid_parsesEvent() throws Exception {
        String secret = "test-secret";
        Field secretField = WavePaymentProvider.class.getDeclaredField("webhookSecret");
        secretField.setAccessible(true);
        secretField.set(provider, secret);

        String json = "{\"type\":\"checkout.session.completed\",\"data\":{\"id\":\"W-EXT-007\",\"client_reference\":\"REF-007\",\"amount\":\"750\"}}";
        String timestamp = "1700000000";
        String payload = timestamp + "." + json;

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
        String computed = HexFormat.of().formatHex(mac.doFinal(payload.getBytes()));

        Map<String, String> headers = Map.of("wave-signature", "t=" + timestamp + ",v1=" + computed);

        PaymentEvent event = provider.processWebhook(json, headers);
        assertThat(event.status()).isEqualTo(PaymentStatus.SUCCESS);
        assertThat(event.externalId()).isEqualTo("W-EXT-007");
    }

    @Test
    void processWebhook_whenSignatureHeaderUsesCapitalCase_isFound() throws Exception {
        String secret = "cap-secret";
        Field secretField = WavePaymentProvider.class.getDeclaredField("webhookSecret");
        secretField.setAccessible(true);
        secretField.set(provider, secret);

        String json = "{\"type\":\"checkout.session.failed\",\"data\":{\"id\":\"W-EXT-008\",\"client_reference\":\"REF-008\",\"amount\":\"300\"}}";
        String timestamp = "1700000001";
        String payload = timestamp + "." + json;

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
        String computed = HexFormat.of().formatHex(mac.doFinal(payload.getBytes()));

        // Use capital "Wave-Signature" header
        Map<String, String> headers = Map.of("Wave-Signature", "t=" + timestamp + ",v1=" + computed);

        PaymentEvent event = provider.processWebhook(json, headers);
        assertThat(event.status()).isEqualTo(PaymentStatus.FAILED);
    }

    @Test
    void processWebhook_whenSecretSetAndSignatureHeaderMissing_throwsPaymentException() throws Exception {
        Field secretField = WavePaymentProvider.class.getDeclaredField("webhookSecret");
        secretField.setAccessible(true);
        secretField.set(provider, "present-secret");

        String json = "{\"type\":\"checkout.session.completed\",\"data\":{\"id\":\"W-EXT-009\",\"client_reference\":\"REF-009\",\"amount\":\"200\"}}";

        assertThatThrownBy(() -> provider.processWebhook(json, Map.of()))
                .isInstanceOf(PaymentException.class)
                .hasMessageContaining("absente");
    }
}
