package dev.lions.unionflow.server.exception;

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

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.core.JsonParser;
import dev.lions.unionflow.server.service.SystemLoggingService;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

/**
 * Tests du {@link GlobalExceptionMapper} : vérification des réponses HTTP
 * selon le type d'exception, en appelant le mapper directement pour limiter
 * le bruit (filtres, OIDC) et les appels REST superflus.
 */
@QuarkusTest
class GlobalExceptionMapperTest {

    @Inject
    GlobalExceptionMapper globalExceptionMapper;

    @InjectMock
    SystemLoggingService systemLoggingService;

    @Test
    @DisplayName("toResponse — systemLoggingService.logError() lève une exception → catch block L77-78 couvert")
    void toResponse_systemLoggingThrows_catchBlockCovered() throws Exception {
        doThrow(new RuntimeException("DB unavailable"))
                .when(systemLoggingService).logError(anyString(), anyString(), anyString(), anyString(), anyString(), anyString(), any(Integer.class));

        // La réponse est construite normalement malgré l'échec du logging
        Response r = globalExceptionMapper.toResponse(new RuntimeException("error during logging test"));
        assertThat(r.getStatus()).isEqualTo(500);
        @SuppressWarnings("unchecked")
        java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
        assertThat(body.get("error")).isEqualTo("Internal server error");
    }

    @Nested
    @DisplayName("Appel direct au mapper")
    class MapperDirect {

        @Test
        @DisplayName("RuntimeException générique → 500")
        void mapRuntimeException_otherRuntime_returns500() {
            Response r = globalExceptionMapper.toResponse(new RuntimeException("inattendu"));
            assertThat(r.getStatus()).isEqualTo(500);
            assertThat(r.getEntity()).isNotNull();
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }

        @Test
        @DisplayName("IllegalArgumentException → 400")
        void mapRuntimeException_illegalArgument_returns400() {
            Response r = globalExceptionMapper.toResponse(new IllegalArgumentException("critère manquant"));
            assertThat(r.getStatus()).isEqualTo(400);
            assertThat(r.getEntity()).isNotNull();
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("critère manquant");
        }

        @Test
        @DisplayName("IllegalStateException → 400 (traité comme BadRequest)")
        void mapRuntimeException_illegalState_returns400() {
            Response r = globalExceptionMapper.toResponse(new IllegalStateException("déjà existant"));
            assertThat(r.getStatus()).isEqualTo(400);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("déjà existant");
        }

        @Test
        @DisplayName("NotFoundException → 404 (cas métier attendu : pas de log ERROR, pas de persistance)")
        void mapRuntimeException_notFound_returns404() {
            Response r = globalExceptionMapper.toResponse(
                    new jakarta.ws.rs.NotFoundException("Ressource introuvable"));
            assertThat(r.getStatus()).isEqualTo(404);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Ressource introuvable");
        }

        @Test
        @DisplayName("ForbiddenException → 403 (cas métier attendu : pas de log ERROR)")
        void mapForbiddenException_returns403() {
            Response r = globalExceptionMapper.toResponse(new ForbiddenException("Accès refusé"));
            assertThat(r.getStatus()).isEqualTo(403);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Accès refusé");
        }

        @Test
        @DisplayName("NotAuthorizedException → 401 (cas métier attendu : pas de log ERROR)")
        void mapNotAuthorizedException_returns401() {
            Response r = globalExceptionMapper.toResponse(
                    new NotAuthorizedException("Session expirée", "Bearer"));
            assertThat(r.getStatus()).isEqualTo(401);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isNotNull();
        }

        @Test
        @DisplayName("WebApplicationException 400 avec message non vide → 400")
        void mapRuntimeException_webApp4xx_withMessage_returns4xx() {
            Response r = globalExceptionMapper.toResponse(
                    new jakarta.ws.rs.WebApplicationException("Bad Request", jakarta.ws.rs.core.Response.status(400).build()));
            assertThat(r.getStatus()).isEqualTo(400);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Bad Request");
        }

        @Test
        @DisplayName("WebApplicationException 404 avec message null → An error occurred")
        void mapRuntimeException_webApp4xx_messageNull_returnsDefaultMessage() {
            Response r = globalExceptionMapper.toResponse(
                    new jakarta.ws.rs.WebApplicationException((String) null, jakarta.ws.rs.core.Response.status(404).build()));
            assertThat(r.getStatus()).isEqualTo(404);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("An error occurred");
        }

        @Test
        @DisplayName("WebApplicationException 403 avec message vide → message vide retourné")
        void mapRuntimeException_webApp4xx_messageEmpty_returnsEmptyMessage() {
            Response r = globalExceptionMapper.toResponse(
                    new jakarta.ws.rs.WebApplicationException("", jakarta.ws.rs.core.Response.status(403).build()));
            assertThat(r.getStatus()).isEqualTo(403);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("");
        }

        @Test
        @DisplayName("WebApplicationException 500 → Internal server error")
        void mapRuntimeException_webApp5xx_returns500() {
            Response r = globalExceptionMapper.toResponse(
                    new jakarta.ws.rs.WebApplicationException("Server Error", jakarta.ws.rs.core.Response.status(500).build()));
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }

        @Test
        @DisplayName("WebApplicationException 399 → retourne le status 399 tel quel")
        void mapRuntimeException_webApp399_returns399() {
            Response r = globalExceptionMapper.toResponse(
                    new jakarta.ws.rs.WebApplicationException("OK", jakarta.ws.rs.core.Response.status(399).build()));
            assertThat(r.getStatus()).isEqualTo(399);
            assertThat(((java.util.Map<?, ?>) r.getEntity()).get("error")).isEqualTo("OK");
        }

        @Test
        @DisplayName("BadRequestException → 400")
        void mapBadRequestException_returns400() {
            Response r = globalExceptionMapper.toResponse(new BadRequestException("requête mal formée"));
            assertThat(r.getStatus()).isEqualTo(400);
            assertThat(r.getEntity()).isNotNull();
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("requête mal formée");
        }

        @Test
        @DisplayName("BadRequestException avec message null → An error occurred")
        void mapBadRequestException_nullMessage_returnsDefaultMessage() {
            Response r = globalExceptionMapper.toResponse(new BadRequestException((String) null));
            assertThat(r.getStatus()).isEqualTo(400);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("An error occurred");
        }

        @Test
        @DisplayName("Exception avec getMessage() null → utilise getClass().getSimpleName() pour le message logué (missed branch L53)")
        void toResponse_exceptionWithNullMessage_usesClassSimpleName() {
            // RuntimeException construit sans message → getMessage() = null
            // → branche false de (exception.getMessage() != null ? ... : exception.getClass().getSimpleName())
            // Note: statusCode=500 → buildErrorResponse retourne "Internal server error" (branch >=500)
            // Mais le message logué utilise getSimpleName() — couvre la branche L53 false
            RuntimeException ex = new RuntimeException((String) null);
            assertThat(ex.getMessage()).isNull(); // sanity check

            Response r = globalExceptionMapper.toResponse(ex);
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            // statusCode >= 500 → "Internal server error"
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }
    }

    @Test
    @DisplayName("toResponse avec uriInfo non-null → endpoint récupéré via getPath() (branche uriInfo!=null L45)")
    void toResponse_withNonNullUriInfo_usesUriInfoPath() throws Exception {
        // Injecter un mock UriInfo via réflexion pour couvrir la branche uriInfo != null (L45)
        UriInfo mockUriInfo = mock(UriInfo.class);
        when(mockUriInfo.getPath()).thenReturn("/api/test/path");

        java.lang.reflect.Field uriInfoField = GlobalExceptionMapper.class.getDeclaredField("uriInfo");
        uriInfoField.setAccessible(true);
        uriInfoField.set(globalExceptionMapper, mockUriInfo);

        try {
            Response r = globalExceptionMapper.toResponse(new RuntimeException("with uri"));
            assertThat(r.getStatus()).isEqualTo(500);
        } finally {
            // Remettre à null pour ne pas affecter les autres tests
            uriInfoField.set(globalExceptionMapper, null);
        }
    }

    @Test
    @DisplayName("toResponse avec uriInfo non-null mais getPath() lève exception → catch block couvert (L48)")
    void toResponse_uriInfoGetPathThrows_catchBlockCovered() throws Exception {
        // Simule le cas où uriInfo est non-null mais getPath() échoue (ex: pas de contexte request actif)
        UriInfo mockUriInfo = mock(UriInfo.class);
        when(mockUriInfo.getPath()).thenThrow(new RuntimeException("ContextNotActive simulé"));

        java.lang.reflect.Field uriInfoField = GlobalExceptionMapper.class.getDeclaredField("uriInfo");
        uriInfoField.setAccessible(true);
        uriInfoField.set(globalExceptionMapper, mockUriInfo);

        try {
            Response r = globalExceptionMapper.toResponse(new IllegalArgumentException("test catch block"));
            // La réponse est construite normalement, l'exception getPath() est ignorée → endpoint reste "unknown"
            assertThat(r.getStatus()).isEqualTo(400);
        } finally {
            uriInfoField.set(globalExceptionMapper, null);
        }
    }

    @Nested
    @DisplayName("JSON exceptions - toResponse traite toutes comme Internal Server Error")
    class MapJsonException {

        /** Sous-classe pour appeler le constructeur protégé MismatchedInputException(JsonParser, String). */
        private static final class StubMismatchedInputException extends MismatchedInputException {
            StubMismatchedInputException() {
                super((JsonParser) null, "msg");
            }
        }

        /** Sous-classe pour appeler le constructeur protégé InvalidFormatException(JsonParser, String, Object, Class). */
        private static final class StubInvalidFormatException extends InvalidFormatException {
            StubInvalidFormatException() {
                super((JsonParser) null, "invalid", null, (Class<?>) null);
            }
        }

        @Test
        @DisplayName("MismatchedInputException → 500 (pas gérée spécifiquement)")
        void mapJsonException_mismatchedInput() {
            Response r = globalExceptionMapper.toResponse(new StubMismatchedInputException());
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }

        @Test
        @DisplayName("InvalidFormatException → 500 (pas gérée spécifiquement)")
        void mapJsonException_invalidFormat() {
            Response r = globalExceptionMapper.toResponse(new StubInvalidFormatException());
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }

        @Test
        @DisplayName("JsonMappingException → 500 (pas gérée spécifiquement)")
        void mapJsonException_jsonMapping() {
            Response r = globalExceptionMapper.toResponse(new JsonMappingException(null, "mapping"));
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }

        @Test
        @DisplayName("JsonParseException → 500 (pas gérée spécifiquement)")
        void mapJsonException_jsonProcessing() {
            Response r = globalExceptionMapper.toResponse(new JsonParseException(null, "parse error"));
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }
    }

    @Nested
    @DisplayName("buildErrorResponse - vérification du format de réponse")
    class BuildResponseBranches {

        @Test
        @DisplayName("Toute exception contient error, status et timestamp")
        void buildResponse_containsRequiredFields() {
            Response r = globalExceptionMapper.toResponse(new BadRequestException((String) null));
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body).containsKeys("error", "status", "timestamp");
        }

        @Test
        @DisplayName("SecurityException → 403 Forbidden")
        void buildResponse_securityException_returns403() {
            Response r = globalExceptionMapper.toResponse(new SecurityException("access denied"));
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(r.getStatus()).isEqualTo(403);
            assertThat(body.get("error")).isEqualTo("access denied");
        }
    }

    @Nested
    @DisplayName("determineSource - branche par nom de classe d'exception")
    class DetermineSourceBranches {

        /** Exception dont le nom de classe simple contient "Database". */
        private static final class DatabaseException extends RuntimeException {
            DatabaseException() { super("db error"); }
        }

        /** Exception dont le nom de classe simple contient "SQL". */
        private static final class SQLQueryException extends RuntimeException {
            SQLQueryException() { super("sql error"); }
        }

        /** Exception dont le nom de classe simple contient "Persistence". */
        private static final class PersistenceException extends RuntimeException {
            PersistenceException() { super("persistence error"); }
        }

        /** Exception dont le nom de classe simple contient "Auth". */
        private static final class AuthException extends RuntimeException {
            AuthException() { super("auth error"); }
        }

        /** Exception dont le nom de classe simple contient "Validation". */
        private static final class ValidationException extends RuntimeException {
            ValidationException() { super("validation error"); }
        }

        @Test
        @DisplayName("Exception nommée 'DatabaseException' → retourne 500 (source=Database)")
        void determineSource_databaseException_returns500() {
            Response r = globalExceptionMapper.toResponse(new DatabaseException());
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }

        @Test
        @DisplayName("Exception nommée 'SQLQueryException' → retourne 500 (source=Database)")
        void determineSource_sqlException_returns500() {
            Response r = globalExceptionMapper.toResponse(new SQLQueryException());
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }

        @Test
        @DisplayName("Exception nommée 'PersistenceException' → retourne 500 (source=Database)")
        void determineSource_persistenceException_returns500() {
            Response r = globalExceptionMapper.toResponse(new PersistenceException());
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }

        @Test
        @DisplayName("Exception nommée 'AuthException' → retourne 500 (source=Auth)")
        void determineSource_authException_returns500() {
            Response r = globalExceptionMapper.toResponse(new AuthException());
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }

        @Test
        @DisplayName("Exception nommée 'ValidationException' → retourne 500 (source=Validation)")
        void determineSource_validationException_returns500() {
            Response r = globalExceptionMapper.toResponse(new ValidationException());
            assertThat(r.getStatus()).isEqualTo(500);
            @SuppressWarnings("unchecked")
            java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
            assertThat(body.get("error")).isEqualTo("Internal server error");
        }
    }

    // =========================================================================
    // catch (IllegalStateException e) — L80-83
    // L80: catch (IllegalStateException e) {
    // L83: log.debug("Cannot persist error log from IO thread...", ...)
    // Le test existant (L47) lève RuntimeException → va dans catch(Exception e) L84-86.
    // Il faut lever une IllegalStateException pour couvrir L80+L83.
    // =========================================================================

    @Test
    @DisplayName("toResponse — systemLoggingService.logError() lève IllegalStateException → catch (IllegalStateException) L80-83 couvert")
    void toResponse_systemLoggingThrowsIllegalState_catchIllegalStateCovered() {
        doThrow(new IllegalStateException("BlockingOperationNotAllowedException simulé"))
                .when(systemLoggingService).logError(anyString(), anyString(), anyString(), anyString(), anyString(), anyString(), any(Integer.class));

        // La réponse doit être construite normalement malgré l'IllegalStateException du logging
        Response r = globalExceptionMapper.toResponse(new RuntimeException("exception test catch IllegalState"));
        assertThat(r.getStatus()).isEqualTo(500);
        @SuppressWarnings("unchecked")
        java.util.Map<String, Object> body = (java.util.Map<String, Object>) r.getEntity();
        assertThat(body.get("error")).isEqualTo("Internal server error");
    }
}
