package de.lions.unionflow.server.auth;

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

import jakarta.ws.rs.core.Response;
import java.lang.reflect.Field;
import org.jboss.logging.Logger;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

/**
 * Couverture du bloc {@code catch(Exception e)} aux lignes 119, 121 et 133 dans
 * {@link AuthCallbackResource#handleCallback}.
 *
 * <p>Ces lignes sont dans le bloc catch qui récupère toute exception levée à l'intérieur
 * du bloc try principal (L31-117) de {@code handleCallback}. Le code du try est entièrement
 * statique (String.formatted, construction de HTML) et ne peut pas échouer avec des paramètres
 * normaux. Pour déclencher le catch, on remplace le {@code private static final Logger log}
 * par un mock qui lève une exception sur l'appel à {@code infof(...)}.
 *
 * <p>Technique : {@code sun.misc.Unsafe.putObject()} via réflexion pour modifier le champ
 * {@code private static final Logger log} sans restriction Java 17. Cette approche est
 * nécessaire car {@code Field.set(null, value)} sur un champ {@code final} lève
 * {@code IllegalAccessException} en Java 9+ même avec {@code setAccessible(true)}.
 *
 * <p>{@code sun.misc.Unsafe} est accessible depuis le module non-nommé (classpath) en Java 17
 * car le module {@code jdk.unsupported} exporte {@code sun.misc} sans restriction.
 *
 * <p>Note : utilise {@code mock-maker-inline} (configuré via
 * {@code src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker}).
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("AuthCallbackResource — catch block L119/121/133 via logger mock (Unsafe putObject)")
class AuthCallbackResourceCatchCoverageTest {

    private Logger originalLogger;
    private Object unsafeInstance;
    private long fieldOffset;
    private Object fieldBase;

    @BeforeEach
    void replaceLoggerWithThrowingMock() throws Exception {
        Field logField = AuthCallbackResource.class.getDeclaredField("log");
        logField.setAccessible(true);
        originalLogger = (Logger) logField.get(null);

        // Obtenir sun.misc.Unsafe via réflexion sur theUnsafe (évite import direct sun.misc.Unsafe
        // pour la compatibilité avec les compilateurs stricts)
        Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
        Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
        theUnsafeField.setAccessible(true);
        unsafeInstance = theUnsafeField.get(null);

        // Calculer l'offset statique du champ et obtenir la base
        java.lang.reflect.Method staticFieldOffsetMethod =
                unsafeClass.getMethod("staticFieldOffset", Field.class);
        java.lang.reflect.Method staticFieldBaseMethod =
                unsafeClass.getMethod("staticFieldBase", Field.class);

        fieldOffset = (long) staticFieldOffsetMethod.invoke(unsafeInstance, logField);
        fieldBase = staticFieldBaseMethod.invoke(unsafeInstance, logField);

        // Créer un logger mock qui throw sur infof() → déclenche le catch à L119
        // L33 appelle: log.infof(String format, code, state, sessionState, error, errorDescription)
        // JBoss Logger a les surcharges suivantes :
        //   infof(String, Object)               → 1 param
        //   infof(String, Object, Object)        → 2 params
        //   infof(String, Object, Object, Object)→ 3 params
        //   infof(String, Object...)             → varargs (5 params → dispatché ici)
        // On intercepte la signature varargs (Object[]) pour couvrir l'appel à 5 params.
        Logger throwingLogger = mock(Logger.class);
        RuntimeException loggerException =
                new RuntimeException("Simulated logger failure — triggers catch block L119");
        // Interception varargs : log.infof(String, Object...) → représenté comme infof(String, Object[])
        doThrow(loggerException)
                .when(throwingLogger).infof(any(String.class), any(Object[].class));

        // Remplacer le logger via Unsafe.putObject (contourne la restriction static final)
        java.lang.reflect.Method putObjectMethod =
                unsafeClass.getMethod("putObject", Object.class, long.class, Object.class);
        putObjectMethod.invoke(unsafeInstance, fieldBase, fieldOffset, throwingLogger);
    }

    @AfterEach
    void restoreOriginalLogger() throws Exception {
        if (originalLogger != null && unsafeInstance != null) {
            Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
            java.lang.reflect.Method putObjectMethod =
                    unsafeClass.getMethod("putObject", Object.class, long.class, Object.class);
            putObjectMethod.invoke(unsafeInstance, fieldBase, fieldOffset, originalLogger);
        }
    }

    // =========================================================================
    // Tests couvrant L119, 121, 133
    // =========================================================================

    /**
     * Couvre L119 ({@code catch(Exception e)}), L121 ({@code String errorHtml = """...}),
     * et L133 ({@code return Response.status(500)...}).
     *
     * <p>Avec le logger mock qui throw sur {@code infof()}, le premier appel à
     * {@code log.infof()} au début du try (L33) déclenche l'exception → le catch L119
     * est atteint → L121 initialise errorHtml → L133 retourne le Response 500.
     */
    @Test
    @DisplayName("handleCallback avec code : log.infof throw → catch L119 → errorHtml L121 → Response 500 L133")
    void handleCallback_loggerThrows_coversCatchL119to133_withCode() {
        AuthCallbackResource resource = new AuthCallbackResource();
        Response response = resource.handleCallback("test-code", "test-state", null, null, null);

        assertThat(response.getStatus()).isEqualTo(500);
        assertThat(response.getMediaType().toString()).contains("text/html");
        assertThat(response.getEntity().toString()).contains("Erreur d'authentification");
        assertThat(response.getEntity().toString()).contains("fermer cette page");
    }

    @Test
    @DisplayName("handleCallback sans paramètres : log.infof throw → catch L119 → Response 500 L133")
    void handleCallback_loggerThrows_coversCatchL119to133_noParams() {
        AuthCallbackResource resource = new AuthCallbackResource();
        Response response = resource.handleCallback(null, null, null, null, null);

        assertThat(response.getStatus()).isEqualTo(500);
        assertThat(response.getMediaType().toString()).contains("text/html");
        assertThat(response.getEntity().toString()).contains("Erreur d'authentification");
    }

    @Test
    @DisplayName("handleCallback avec error : log.infof throw → catch L119 → Response 500 L133")
    void handleCallback_loggerThrows_coversCatchL119to133_withError() {
        AuthCallbackResource resource = new AuthCallbackResource();
        Response response = resource.handleCallback(null, null, null, "access_denied", "User denied");

        assertThat(response.getStatus()).isEqualTo(500);
        assertThat(response.getMediaType().toString()).contains("text/html");
        assertThat(response.getEntity().toString()).contains("Erreur d'authentification");
    }

    @Test
    @DisplayName("handleCallback avec code vide + session_state : log.infof throw → catch L119 → Response 500")
    void handleCallback_loggerThrows_coversCatchL119to133_emptyCodeWithSessionState() {
        AuthCallbackResource resource = new AuthCallbackResource();
        Response response = resource.handleCallback("", null, "session-abc", null, null);

        assertThat(response.getStatus()).isEqualTo(500);
        assertThat(response.getMediaType().toString()).contains("text/html");
        assertThat(response.getEntity().toString()).contains("Erreur d'authentification");
    }
}
