package dev.lions.unionflow.server.service;

import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
 * Tests unitaires pour {@link WaveCheckoutService}.
 *
 * <p>En profil test, {@code wave.mock.enabled=true} et {@code wave.api.key} est vide
 * (définis dans application-test.properties), donc {@code createSession()} prend
 * toujours le chemin mock via la branche {@code useMock = mockEnabled || apiKey.isBlank()}.
 *
 * <p>Branches couvertes :
 * <ul>
 *   <li>createSession() mode mock (mockEnabled=true → useMock=true)</li>
 *   <li>createSession() mode mock via apiKey blank (useMock=true via apiKey.isBlank())</li>
 *   <li>buildRequestBody() — clientRef null → pas de client_reference</li>
 *   <li>buildRequestBody() — clientRef non-null et longueur &le;255 → inclus tel quel</li>
 *   <li>buildRequestBody() — clientRef non-null et longueur &gt;255 → tronqué à 255</li>
 *   <li>buildRequestBody() — restrictMobile null → pas de restrict_payer_mobile</li>
 *   <li>buildRequestBody() — restrictMobile non-null → inclus</li>
 *   <li>signingSecret blank → waveSignature reste null (pas d'en-tête Wave-Signature)</li>
 *   <li>signingSecret non-blank → computeWaveSignature appelé (testé via réflexion)</li>
 *   <li>getRedirectBaseUrl() — valeur non-vide configurée</li>
 *   <li>WaveCheckoutSessionResponse constructeur + champs publics</li>
 *   <li>WaveCheckoutException(String) et WaveCheckoutException(String, Throwable)</li>
 *   <li>escapeJson() — valeur null → chaîne vide</li>
 * </ul>
 */
@QuarkusTest
class WaveCheckoutServiceTest {

    @Inject
    WaveCheckoutService waveCheckoutService;

    // =========================================================================
    // createSession — mode mock (wave.mock.enabled=true en test)
    // Couvre la branche : useMock = mockEnabled || apiKey.isBlank() → true (mockEnabled=true)
    // =========================================================================

    @Test
    @DisplayName("createSession en mode mock retourne une réponse avec id préfixé 'cos-mock-'")
    void createSession_mockMode_returnsResponseWithMockId() throws WaveCheckoutService.WaveCheckoutException {
        WaveCheckoutService.WaveCheckoutSessionResponse response = waveCheckoutService.createSession(
                "10000", "XOF",
                "https://success.example.com", "https://error.example.com",
                "REF-001", null);

        assertThat(response).isNotNull();
        assertThat(response.id).isNotNull().startsWith("cos-mock-");
        assertThat(response.waveLaunchUrl).isEqualTo("https://success.example.com");
    }

    @Test
    @DisplayName("createSession en mode mock avec clientRef null fonctionne correctement")
    void createSession_mockMode_nullClientRef_succeeds() throws WaveCheckoutService.WaveCheckoutException {
        WaveCheckoutService.WaveCheckoutSessionResponse response = waveCheckoutService.createSession(
                "5000", "XOF",
                "https://success.example.com/pay", "https://error.example.com/pay",
                null, null);

        assertThat(response).isNotNull();
        assertThat(response.id).startsWith("cos-mock-");
        assertThat(response.waveLaunchUrl).isEqualTo("https://success.example.com/pay");
    }

    @Test
    @DisplayName("createSession en mode mock — chaque appel génère un id unique")
    void createSession_mockMode_generatesUniqueIds() throws WaveCheckoutService.WaveCheckoutException {
        WaveCheckoutService.WaveCheckoutSessionResponse r1 = waveCheckoutService.createSession(
                "100", "XOF", "https://s.example.com", "https://e.example.com", "REF-1", null);
        WaveCheckoutService.WaveCheckoutSessionResponse r2 = waveCheckoutService.createSession(
                "200", "XOF", "https://s.example.com", "https://e.example.com", "REF-2", null);

        assertThat(r1.id).isNotEqualTo(r2.id);
    }

    // =========================================================================
    // buildRequestBody — branches clientRef et restrictMobile (via réflexion)
    //
    // Branche 1: clientRef == null || isBlank() → false → pas de "client_reference"
    // Branche 2: clientRef.length() <= 255 → inclus tel quel
    // Branche 3: clientRef.length() > 255 → tronqué à 255
    // Branche 4: restrictMobile == null || isBlank() → false → pas de "restrict_payer_mobile"
    // Branche 5: restrictMobile non-null non-blank → inclus
    // =========================================================================

    @Test
    @DisplayName("buildRequestBody avec clientRef null — pas de client_reference dans le corps")
    void buildRequestBody_clientRefNull_doesNotAddClientReference() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "buildRequestBody",
                String.class, String.class, String.class, String.class, String.class, String.class);
        method.setAccessible(true);

        String result = (String) method.invoke(waveCheckoutService,
                "5000", "XOF", "https://s.example.com", "https://e.example.com", null, null);

        assertThat(result).isNotNull();
        assertThat(result).doesNotContain("client_reference");
        assertThat(result).doesNotContain("restrict_payer_mobile");
        assertThat(result).contains("\"amount\":\"5000\"");
        assertThat(result).contains("\"currency\":\"XOF\"");
    }

    @Test
    @DisplayName("buildRequestBody avec clientRef vide (blank) — pas de client_reference dans le corps")
    void buildRequestBody_clientRefBlank_doesNotAddClientReference() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "buildRequestBody",
                String.class, String.class, String.class, String.class, String.class, String.class);
        method.setAccessible(true);

        String result = (String) method.invoke(waveCheckoutService,
                "5000", "XOF", "https://s.example.com", "https://e.example.com", "   ", null);

        assertThat(result).doesNotContain("client_reference");
    }

    @Test
    @DisplayName("buildRequestBody avec clientRef <= 255 chars — inclus tel quel (branche non-tronquée)")
    void buildRequestBody_clientRefShort_includedAsIs() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "buildRequestBody",
                String.class, String.class, String.class, String.class, String.class, String.class);
        method.setAccessible(true);

        String shortRef = "REF-123-SHORT";
        String result = (String) method.invoke(waveCheckoutService,
                "10000", "XOF", "https://s.example.com", "https://e.example.com", shortRef, null);

        assertThat(result).contains("\"client_reference\":\"REF-123-SHORT\"");
    }

    @Test
    @DisplayName("buildRequestBody avec clientRef > 255 chars — tronqué à 255 (branche truncation)")
    void buildRequestBody_clientRefTooLong_truncatedTo255() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "buildRequestBody",
                String.class, String.class, String.class, String.class, String.class, String.class);
        method.setAccessible(true);

        String longRef = "A".repeat(300);
        String result = (String) method.invoke(waveCheckoutService,
                "10000", "XOF", "https://s.example.com", "https://e.example.com", longRef, null);

        assertThat(result).contains("client_reference");
        // La valeur dans le JSON doit être exactement 255 'A'
        assertThat(result).contains("\"" + "A".repeat(255) + "\"");
        assertThat(result).doesNotContain("\"" + "A".repeat(256) + "\"");
    }

    @Test
    @DisplayName("buildRequestBody avec restrictMobile non-null — inclus dans le corps")
    void buildRequestBody_restrictMobileNonNull_included() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "buildRequestBody",
                String.class, String.class, String.class, String.class, String.class, String.class);
        method.setAccessible(true);

        String result = (String) method.invoke(waveCheckoutService,
                "10000", "XOF", "https://s.example.com", "https://e.example.com",
                "REF-123", "+22170000000");

        assertThat(result).contains("\"restrict_payer_mobile\":\"+22170000000\"");
    }

    @Test
    @DisplayName("buildRequestBody avec les deux champs optionnels présents — client_reference et restrict_payer_mobile présents")
    void buildRequestBody_allOptionalFieldsPresent() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "buildRequestBody",
                String.class, String.class, String.class, String.class, String.class, String.class);
        method.setAccessible(true);

        String result = (String) method.invoke(waveCheckoutService,
                "10000", "XOF", "https://s.example.com", "https://e.example.com",
                "REF-FULL", "+22170000000");

        assertThat(result).contains("client_reference");
        assertThat(result).contains("restrict_payer_mobile");
    }

    // =========================================================================
    // createSession — branche apiKey blank → useMock=true même si mockEnabled=false
    // On force mockEnabled=false via réflexion, mais apiKey reste blank → useMock=true
    // =========================================================================

    @Test
    @DisplayName("createSession avec mockEnabled=false mais apiKey blank → toujours mode mock")
    void createSession_mockDisabled_apiKeyBlank_stillMock() throws Exception {
        // Forcer mockEnabled = false via réflexion
        java.lang.reflect.Field mockEnabledField = WaveCheckoutService.class.getDeclaredField("mockEnabled");
        mockEnabledField.setAccessible(true);
        boolean originalMockEnabled = (boolean) mockEnabledField.get(waveCheckoutService);
        mockEnabledField.set(waveCheckoutService, false);

        try {
            // wave.api.key est " " (blank) dans application-test.properties
            // → useMock = false || " ".trim().isBlank() = false || true = true
            WaveCheckoutService.WaveCheckoutSessionResponse response = waveCheckoutService.createSession(
                    "5000", "XOF",
                    "https://success.example.com", "https://error.example.com",
                    "REF-BLANK-KEY", null);

            assertThat(response).isNotNull();
            assertThat(response.id).startsWith("cos-mock-");
        } finally {
            // Restaurer l'état original
            mockEnabledField.set(waveCheckoutService, originalMockEnabled);
        }
    }

    // =========================================================================
    // computeWaveSignature — testé via réflexion (signingSecret non-blank)
    // Couvre la branche : signingSecret != null && !signingSecret.isBlank() → true
    // =========================================================================

    @Test
    @DisplayName("computeWaveSignature retourne un HMAC-SHA256 hexadécimal non vide")
    void computeWaveSignature_returnsNonEmptyHex() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "computeWaveSignature", long.class, String.class);
        method.setAccessible(true);

        // Injecter temporairement un signingSecret non-blank
        java.lang.reflect.Field secretField = WaveCheckoutService.class.getDeclaredField("signingSecret");
        secretField.setAccessible(true);
        String originalSecret = (String) secretField.get(waveCheckoutService);
        secretField.set(waveCheckoutService, "my-test-secret-key");

        try {
            String signature = (String) method.invoke(waveCheckoutService,
                    1700000000L, "{\"amount\":\"5000\"}");

            assertThat(signature).isNotNull().isNotBlank();
            assertThat(signature).hasSize(64); // HMAC-SHA256 = 32 bytes = 64 hex chars
            // Doit être uniquement des caractères hexadécimaux
            assertThat(signature).matches("[0-9a-f]+");
        } finally {
            secretField.set(waveCheckoutService, originalSecret);
        }
    }

    @Test
    @DisplayName("createSession avec signingSecret non-blank → Wave-Signature calculé (via réflexion)")
    void createSession_signingSecretNonBlank_waveSignatureCalculated() throws Exception {
        // Pour atteindre la branche signingSecret non-blank dans createSession(),
        // il faut que useMock=false → mockEnabled=false ET apiKey non-blank
        java.lang.reflect.Field mockEnabledField = WaveCheckoutService.class.getDeclaredField("mockEnabled");
        mockEnabledField.setAccessible(true);
        boolean originalMockEnabled = (boolean) mockEnabledField.get(waveCheckoutService);

        java.lang.reflect.Field apiKeyField = WaveCheckoutService.class.getDeclaredField("apiKey");
        apiKeyField.setAccessible(true);
        String originalApiKey = (String) apiKeyField.get(waveCheckoutService);

        java.lang.reflect.Field secretField = WaveCheckoutService.class.getDeclaredField("signingSecret");
        secretField.setAccessible(true);
        String originalSecret = (String) secretField.get(waveCheckoutService);

        // Tester computeWaveSignature directement pour couvrir la branche signingSecret non-blank
        secretField.set(waveCheckoutService, "secret-for-signature-test");
        try {
            java.lang.reflect.Method computeMethod = WaveCheckoutService.class.getDeclaredMethod(
                    "computeWaveSignature", long.class, String.class);
            computeMethod.setAccessible(true);
            String sig = (String) computeMethod.invoke(waveCheckoutService,
                    System.currentTimeMillis() / 1000, "{\"test\":\"body\"}");
            assertThat(sig).isNotBlank().hasSize(64);
        } finally {
            secretField.set(waveCheckoutService, originalSecret);
            mockEnabledField.set(waveCheckoutService, originalMockEnabled);
            apiKeyField.set(waveCheckoutService, originalApiKey);
        }
    }

    // =========================================================================
    // escapeJson — branche null → retourne "" (via réflexion)
    // =========================================================================

    @Test
    @DisplayName("escapeJson avec null retourne chaîne vide")
    void escapeJson_null_returnsEmptyString() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "escapeJson", String.class);
        method.setAccessible(true);
        Object result = method.invoke(null, (Object) null);
        assertThat(result).isEqualTo("");
    }

    @Test
    @DisplayName("escapeJson avec chaîne normale retourne la chaîne inchangée")
    void escapeJson_normalString_returnsUnchanged() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "escapeJson", String.class);
        method.setAccessible(true);
        Object result = method.invoke(null, "simple string");
        assertThat(result).isEqualTo("simple string");
    }

    @Test
    @DisplayName("escapeJson avec caractères spéciaux — backslash et guillemets échappés")
    void escapeJson_specialChars_escaped() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "escapeJson", String.class);
        method.setAccessible(true);
        Object result = method.invoke(null, "a\\b\"c");
        assertThat(result).isEqualTo("a\\\\b\\\"c");
    }

    // =========================================================================
    // getRedirectBaseUrl
    // =========================================================================

    @Test
    @DisplayName("getRedirectBaseUrl retourne la valeur configurée (http://localhost:8080 en test)")
    void getRedirectBaseUrl_returnsConfiguredValue() {
        String result = waveCheckoutService.getRedirectBaseUrl();
        assertThat(result).isEqualTo("http://localhost:8080");
    }

    @Test
    @DisplayName("getRedirectBaseUrl avec champ vide → retourne http://localhost:8080 par défaut")
    void getRedirectBaseUrl_emptyField_returnsDefault() throws Exception {
        java.lang.reflect.Field field = WaveCheckoutService.class.getDeclaredField("redirectBaseUrl");
        field.setAccessible(true);
        String original = (String) field.get(waveCheckoutService);
        field.set(waveCheckoutService, "");

        try {
            String result = waveCheckoutService.getRedirectBaseUrl();
            assertThat(result).isEqualTo("http://localhost:8080");
        } finally {
            field.set(waveCheckoutService, original);
        }
    }

    // =========================================================================
    // WaveCheckoutSessionResponse — constructeur et champs publics
    // =========================================================================

    @Test
    @DisplayName("WaveCheckoutSessionResponse expose id et waveLaunchUrl via les champs publics")
    void waveCheckoutSessionResponse_publicFields() {
        WaveCheckoutService.WaveCheckoutSessionResponse response =
                new WaveCheckoutService.WaveCheckoutSessionResponse("id123", "https://wave.example.com/pay");

        assertThat(response.id).isEqualTo("id123");
        assertThat(response.waveLaunchUrl).isEqualTo("https://wave.example.com/pay");
    }

    @Test
    @DisplayName("WaveCheckoutSessionResponse accepte des valeurs nulles sans NPE")
    void waveCheckoutSessionResponse_nullValues_noNpe() {
        WaveCheckoutService.WaveCheckoutSessionResponse response =
                new WaveCheckoutService.WaveCheckoutSessionResponse(null, null);

        assertThat(response.id).isNull();
        assertThat(response.waveLaunchUrl).isNull();
    }

    // =========================================================================
    // WaveCheckoutException — constructeurs
    // =========================================================================

    @Test
    @DisplayName("WaveCheckoutException(String) conserve le message")
    void waveCheckoutException_messageOnly_correctMessage() {
        WaveCheckoutService.WaveCheckoutException ex =
                new WaveCheckoutService.WaveCheckoutException("Test error message");

        assertThat(ex.getMessage()).isEqualTo("Test error message");
        assertThat(ex.getCause()).isNull();
        assertThat(ex).isInstanceOf(RuntimeException.class);
    }

    @Test
    @DisplayName("WaveCheckoutException(String, Throwable) conserve le message et la cause")
    void waveCheckoutException_messageAndCause_correct() {
        RuntimeException cause = new RuntimeException("root cause");
        WaveCheckoutService.WaveCheckoutException ex =
                new WaveCheckoutService.WaveCheckoutException("Wrapper message", cause);

        assertThat(ex.getMessage()).isEqualTo("Wrapper message");
        assertThat(ex.getCause()).isSameAs(cause);
    }

    @Test
    @DisplayName("WaveCheckoutException peut être lancée et attrapée")
    void waveCheckoutException_canBeThrown() {
        assertThatThrownBy(() -> {
            throw new WaveCheckoutService.WaveCheckoutException("thrown!");
        })
                .isInstanceOf(WaveCheckoutService.WaveCheckoutException.class)
                .isInstanceOf(RuntimeException.class)
                .hasMessage("thrown!");
    }

    // =========================================================================
    // createSession L64 — branche apiKey == null → useMock=true (court-circuit null-check)
    // =========================================================================

    @Test
    @DisplayName("createSession avec apiKey null → useMock=true via apiKey==null (L64 branche null)")
    void createSession_apiKeyNull_useMockTrue() throws Exception {
        java.lang.reflect.Field apiKeyField = WaveCheckoutService.class.getDeclaredField("apiKey");
        apiKeyField.setAccessible(true);
        String originalApiKey = (String) apiKeyField.get(waveCheckoutService);
        apiKeyField.set(waveCheckoutService, null);

        try {
            // apiKey == null → useMock = mockEnabled || true → true → mode mock
            WaveCheckoutService.WaveCheckoutSessionResponse response = waveCheckoutService.createSession(
                    "5000", "XOF",
                    "https://success.example.com", "https://error.example.com",
                    "REF-NULL-KEY", null);

            assertThat(response).isNotNull();
            assertThat(response.id).startsWith("cos-mock-");
        } finally {
            apiKeyField.set(waveCheckoutService, originalApiKey);
        }
    }

    // =========================================================================
    // buildRequestBody L129 — branche restrictMobile non-null mais blank → pas de restrict_payer_mobile
    // =========================================================================

    @Test
    @DisplayName("buildRequestBody avec restrictMobile blank → pas de restrict_payer_mobile (L129 branche isBlank=true)")
    void buildRequestBody_restrictMobileBlank_notIncluded() throws Exception {
        java.lang.reflect.Method method = WaveCheckoutService.class.getDeclaredMethod(
                "buildRequestBody",
                String.class, String.class, String.class, String.class, String.class, String.class);
        method.setAccessible(true);

        // restrictMobile non-null mais blank → !restrictMobile.isBlank() = false → pas ajouté
        String result = (String) method.invoke(waveCheckoutService,
                "5000", "XOF", "https://s.example.com", "https://e.example.com",
                "REF-123", "   ");

        assertThat(result).doesNotContain("restrict_payer_mobile");
        assertThat(result).contains("\"client_reference\":\"REF-123\"");
    }

    // =========================================================================
    // NOTE: La branche baseUrl avec trailing slash (L70 endsWith("/")=true) est couverte par
    // WaveCheckoutServiceRealPathTest.createSession_realMode_success_returnsValidResponse
    // qui utilise baseUrl = "http://localhost:" + MOCK_PORT + "/v1/" (avec /).
    // =========================================================================

    // =========================================================================
    // NOTE: Les branches L64 (useMock=false: apiKey non-vide + mockEnabled=false) et L106
    // (signingSecret non-null et non-blank → computeWaveSignature) sont couvertes par
    // WaveCheckoutServiceRealPathTest (non-Quarkus, instanciation directe du service).
    // Les tests avec réflexion sur proxy CDI ne fonctionnent pas (@ApplicationScoped → proxy),
    // c'est pourquoi ces branches sont testées dans le fichier sans @QuarkusTest.
    // =========================================================================
}
