package dev.lions.unionflow.server.filter;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

import dev.lions.unionflow.server.service.SystemLoggingService;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.core.UriInfo;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.Principal;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
 * Tests unitaires pour HttpLoggingFilter (sans @QuarkusTest pour contrôle total).
 * Couvre les méthodes privées et toutes les branches via réflexion + mocks Mockito.
 */
class HttpLoggingFilterTest {

    HttpLoggingFilter filter;
    SystemLoggingService mockLoggingService;

    @BeforeEach
    void setUp() throws Exception {
        filter = new HttpLoggingFilter();
        mockLoggingService = mock(SystemLoggingService.class);

        Field field = HttpLoggingFilter.class.getDeclaredField("systemLoggingService");
        field.setAccessible(true);
        field.set(filter, mockLoggingService);
    }

    // =========================================================================
    // filter(ContainerRequestContext) — request filter
    // =========================================================================

    @Test
    @DisplayName("filter request — enregistre startTime, method et path dans les propriétés")
    void requestFilter_setsProperties() throws Exception {
        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        UriInfo uriInfo = mock(UriInfo.class);
        when(ctx.getUriInfo()).thenReturn(uriInfo);
        when(uriInfo.getPath()).thenReturn("api/test");
        when(ctx.getMethod()).thenReturn("GET");

        assertThatCode(() -> filter.filter(ctx)).doesNotThrowAnyException();

        verify(ctx).setProperty(eq("REQUEST_START_TIME"), anyLong());
        verify(ctx).setProperty("REQUEST_METHOD", "GET");
        verify(ctx).setProperty("REQUEST_PATH", "api/test");
    }

    // =========================================================================
    // filter(request, response) — response filter avec path api/ → shouldLog=true
    // =========================================================================

    @Test
    @DisplayName("filter response — path api/ → shouldLog true → logRequest appelé")
    void responseFilter_apiPath_logsRequest() throws Exception {
        ContainerRequestContext reqCtx = mock(ContainerRequestContext.class);
        ContainerResponseContext resCtx = mock(ContainerResponseContext.class);

        when(reqCtx.getProperty("REQUEST_START_TIME")).thenReturn(System.currentTimeMillis() - 100);
        when(reqCtx.getProperty("REQUEST_METHOD")).thenReturn("GET");
        when(reqCtx.getProperty("REQUEST_PATH")).thenReturn("api/membres");
        when(resCtx.getStatus()).thenReturn(200);
        when(reqCtx.getSecurityContext()).thenReturn(null);
        when(reqCtx.getHeaderString(anyString())).thenReturn(null);

        filter.filter(reqCtx, resCtx);

        verify(mockLoggingService).logRequest(anyString(), anyString(), anyInt(), anyString(), anyString(), any(), anyLong());
    }

    // =========================================================================
    // filter(request, response) — startTime null → durationMs = 0
    // =========================================================================

    @Test
    @DisplayName("filter response — startTime null → durationMs = 0")
    void responseFilter_nullStartTime_durationIsZero() throws Exception {
        ContainerRequestContext reqCtx = mock(ContainerRequestContext.class);
        ContainerResponseContext resCtx = mock(ContainerResponseContext.class);

        when(reqCtx.getProperty("REQUEST_START_TIME")).thenReturn(null);
        when(reqCtx.getProperty("REQUEST_METHOD")).thenReturn("POST");
        when(reqCtx.getProperty("REQUEST_PATH")).thenReturn("api/test");
        when(resCtx.getStatus()).thenReturn(201);
        when(reqCtx.getSecurityContext()).thenReturn(null);
        when(reqCtx.getHeaderString(anyString())).thenReturn(null);

        filter.filter(reqCtx, resCtx);

        verify(mockLoggingService).logRequest(eq("POST"), eq("/api/test"), eq(201), eq("anonymous"), eq("unknown"), isNull(), eq(0L));
    }

    // =========================================================================
    // filter(request, response) — path q/ → shouldLog false → pas de log
    // =========================================================================

    @Test
    @DisplayName("filter response — path q/ → shouldLog false → logRequest non appelé")
    void responseFilter_qPath_doesNotLog() throws Exception {
        ContainerRequestContext reqCtx = mock(ContainerRequestContext.class);
        ContainerResponseContext resCtx = mock(ContainerResponseContext.class);

        when(reqCtx.getProperty("REQUEST_START_TIME")).thenReturn(System.currentTimeMillis());
        when(reqCtx.getProperty("REQUEST_METHOD")).thenReturn("GET");
        when(reqCtx.getProperty("REQUEST_PATH")).thenReturn("q/health");
        when(resCtx.getStatus()).thenReturn(200);

        filter.filter(reqCtx, resCtx);

        verify(mockLoggingService, never()).logRequest(any(), any(), anyInt(), any(), any(), any(), anyLong());
    }

    // =========================================================================
    // shouldLog — branches
    // =========================================================================

    @Test
    @DisplayName("shouldLog null → false")
    void shouldLog_null_returnsFalse() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("shouldLog", String.class);
        m.setAccessible(true);
        assertThat((Boolean) m.invoke(filter, (String) null)).isFalse();
    }

    @Test
    @DisplayName("shouldLog 'q/health' → false")
    void shouldLog_qPath_returnsFalse() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("shouldLog", String.class);
        m.setAccessible(true);
        assertThat((Boolean) m.invoke(filter, "q/health")).isFalse();
    }

    @Test
    @DisplayName("shouldLog 'static/img.png' → false")
    void shouldLog_staticPath_returnsFalse() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("shouldLog", String.class);
        m.setAccessible(true);
        assertThat((Boolean) m.invoke(filter, "static/img.png")).isFalse();
    }

    @Test
    @DisplayName("shouldLog 'webjars/jquery.js' → false")
    void shouldLog_webjarsPath_returnsFalse() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("shouldLog", String.class);
        m.setAccessible(true);
        assertThat((Boolean) m.invoke(filter, "webjars/jquery.js")).isFalse();
    }

    @Test
    @DisplayName("shouldLog 'api/membres' → true")
    void shouldLog_apiPath_returnsTrue() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("shouldLog", String.class);
        m.setAccessible(true);
        assertThat((Boolean) m.invoke(filter, "api/membres")).isTrue();
    }

    @Test
    @DisplayName("shouldLog 'other/path' → false (ne commence pas par api/)")
    void shouldLog_otherPath_returnsFalse() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("shouldLog", String.class);
        m.setAccessible(true);
        assertThat((Boolean) m.invoke(filter, "other/path")).isFalse();
    }

    // =========================================================================
    // extractUserId — branches
    // =========================================================================

    @Test
    @DisplayName("extractUserId — securityContext null → 'anonymous'")
    void extractUserId_nullSecurityContext_returnsAnonymous() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractUserId", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        when(ctx.getSecurityContext()).thenReturn(null);

        assertThat((String) m.invoke(filter, ctx)).isEqualTo("anonymous");
    }

    @Test
    @DisplayName("extractUserId — securityContext présent mais principal null → 'anonymous'")
    void extractUserId_nullPrincipal_returnsAnonymous() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractUserId", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        SecurityContext sc = mock(SecurityContext.class);
        when(ctx.getSecurityContext()).thenReturn(sc);
        when(sc.getUserPrincipal()).thenReturn(null);

        assertThat((String) m.invoke(filter, ctx)).isEqualTo("anonymous");
    }

    @Test
    @DisplayName("extractUserId — principal non null → retourne le nom du principal")
    void extractUserId_withPrincipal_returnsPrincipalName() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractUserId", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        SecurityContext sc = mock(SecurityContext.class);
        Principal principal = mock(Principal.class);
        when(ctx.getSecurityContext()).thenReturn(sc);
        when(sc.getUserPrincipal()).thenReturn(principal);
        when(principal.getName()).thenReturn("alice@test.com");

        assertThat((String) m.invoke(filter, ctx)).isEqualTo("alice@test.com");
    }

    // =========================================================================
    // extractIpAddress — branches
    // =========================================================================

    @Test
    @DisplayName("extractIpAddress — X-Forwarded-For présent → première IP")
    void extractIpAddress_xForwardedFor_returnsFirstIp() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractIpAddress", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        when(ctx.getHeaderString("X-Forwarded-For")).thenReturn("10.0.0.1, 10.0.0.2");
        when(ctx.getHeaderString("X-Real-IP")).thenReturn(null);

        assertThat((String) m.invoke(filter, ctx)).isEqualTo("10.0.0.1");
    }

    @Test
    @DisplayName("extractIpAddress — X-Real-IP présent → retourne X-Real-IP")
    void extractIpAddress_xRealIp_returnsRealIp() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractIpAddress", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        when(ctx.getHeaderString("X-Forwarded-For")).thenReturn(null);
        when(ctx.getHeaderString("X-Real-IP")).thenReturn("192.168.1.50");

        assertThat((String) m.invoke(filter, ctx)).isEqualTo("192.168.1.50");
    }

    @Test
    @DisplayName("extractIpAddress — aucun header → 'unknown'")
    void extractIpAddress_noHeaders_returnsUnknown() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractIpAddress", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        when(ctx.getHeaderString("X-Forwarded-For")).thenReturn(null);
        when(ctx.getHeaderString("X-Real-IP")).thenReturn(null);

        assertThat((String) m.invoke(filter, ctx)).isEqualTo("unknown");
    }

    // =========================================================================
    // extractSessionId — branches
    // =========================================================================

    @Test
    @DisplayName("extractSessionId — X-Session-ID présent → retourne la valeur")
    void extractSessionId_withHeader_returnsSessionId() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractSessionId", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        when(ctx.getHeaderString("X-Session-ID")).thenReturn("session-abc-123");

        assertThat((String) m.invoke(filter, ctx)).isEqualTo("session-abc-123");
    }

    @Test
    @DisplayName("extractSessionId — X-Session-ID absent → null")
    void extractSessionId_noHeader_returnsNull() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractSessionId", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        when(ctx.getHeaderString("X-Session-ID")).thenReturn(null);

        assertThat((String) m.invoke(filter, ctx)).isNull();
    }

    // =========================================================================
    // extractIpAddress — branches manquantes: header non-null mais vide
    // =========================================================================

    @Test
    @DisplayName("extractIpAddress — X-Forwarded-For vide (isEmpty) → tente X-Real-IP")
    void extractIpAddress_emptyXForwardedFor_fallsThrough() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractIpAddress", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        // X-Forwarded-For non-null mais vide → condition !xForwardedFor.isEmpty() = false → tente X-Real-IP
        when(ctx.getHeaderString("X-Forwarded-For")).thenReturn("");
        when(ctx.getHeaderString("X-Real-IP")).thenReturn("10.10.10.10");

        assertThat((String) m.invoke(filter, ctx)).isEqualTo("10.10.10.10");
    }

    @Test
    @DisplayName("extractIpAddress — X-Forwarded-For vide + X-Real-IP vide → 'unknown'")
    void extractIpAddress_emptyXForwardedFor_emptyXRealIp_returnsUnknown() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractIpAddress", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        // Les deux headers non-null mais vides → condition !isEmpty() = false pour les deux → "unknown"
        when(ctx.getHeaderString("X-Forwarded-For")).thenReturn("");
        when(ctx.getHeaderString("X-Real-IP")).thenReturn("");

        assertThat((String) m.invoke(filter, ctx)).isEqualTo("unknown");
    }

    @Test
    @DisplayName("extractSessionId — X-Session-ID vide (isEmpty) → null")
    void extractSessionId_emptyHeader_returnsNull() throws Exception {
        Method m = HttpLoggingFilter.class.getDeclaredMethod("extractSessionId", ContainerRequestContext.class);
        m.setAccessible(true);

        ContainerRequestContext ctx = mock(ContainerRequestContext.class);
        // X-Session-ID non-null mais vide → retourne null
        when(ctx.getHeaderString("X-Session-ID")).thenReturn("");

        assertThat((String) m.invoke(filter, ctx)).isNull();
    }

    // =========================================================================
    // filter(request, response) — catch block quand logRequest lève une exception
    // =========================================================================

    @Test
    @DisplayName("filter response — exception dans logRequest est capturée (catch block)")
    void responseFilter_exceptionInLog_isCaught() throws Exception {
        ContainerRequestContext reqCtx = mock(ContainerRequestContext.class);
        ContainerResponseContext resCtx = mock(ContainerResponseContext.class);

        when(reqCtx.getProperty("REQUEST_START_TIME")).thenReturn(System.currentTimeMillis());
        when(reqCtx.getProperty("REQUEST_METHOD")).thenReturn("GET");
        when(reqCtx.getProperty("REQUEST_PATH")).thenReturn("api/test");
        when(resCtx.getStatus()).thenReturn(200);
        when(reqCtx.getSecurityContext()).thenReturn(null);
        when(reqCtx.getHeaderString(anyString())).thenReturn(null);
        doThrow(new RuntimeException("log error")).when(mockLoggingService)
                .logRequest(any(), any(), anyInt(), any(), any(), any(), anyLong());

        // Ne doit pas propager l'exception (catch block)
        assertThatCode(() -> filter.filter(reqCtx, resCtx)).doesNotThrowAnyException();
    }
}
