package dev.lions.unionflow.server.service;

import com.fasterxml.jackson.databind.ObjectMapper;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedConstruction;
import org.mockito.junit.jupiter.MockitoExtension;

import java.lang.reflect.Field;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

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

/**
 * Unit tests for KeycloakAdminHttpClient.
 *
 * Because HttpClient is created as a final field (not injectable), we use
 * reflection to replace it with a mock after instantiation.
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("Tests KeycloakAdminHttpClient")
class KeycloakAdminHttpClientTest {

    // We instantiate the service manually to inject the mock HttpClient via reflection
    private KeycloakAdminHttpClient service;

    @Mock
    private HttpClient mockHttpClient;

    @SuppressWarnings("unchecked")
    private HttpResponse<String> mockResponse(int statusCode, String body) {
        HttpResponse<String> resp = mock(HttpResponse.class);
        when(resp.statusCode()).thenReturn(statusCode);
        // body() is only read on error paths or when parsing JSON — make lenient
        lenient().when(resp.body()).thenReturn(body);
        return resp;
    }

    @BeforeEach
    void setUp() throws Exception {
        service = new KeycloakAdminHttpClient();

        // Inject config values via reflection
        setField(service, "keycloakUrl", "http://localhost:8180");
        setField(service, "adminUsername", "admin");
        setField(service, "adminPassword", "admin");
        setField(service, "realm", "unionflow");

        // Replace the final httpClient field with our mock
        setField(service, "httpClient", mockHttpClient);
    }

    private void setField(Object target, String fieldName, Object value) throws Exception {
        Field f = KeycloakAdminHttpClient.class.getDeclaredField(fieldName);
        f.setAccessible(true);
        f.set(target, value);
    }

    // ── logoutAllSessions — happy path ─────────────────────────────────────────

    @Test
    @DisplayName("logoutAllSessions — should logout all users and return count")
    void logoutAllSessions_happyPath_returnsCount() throws Exception {
        // Token response
        HttpResponse<String> tokenResp = mockResponse(200,
                "{\"access_token\":\"my-token-123\"}");

        // Users response: 2 users
        String usersJson = "[{\"id\":\"uid-1\",\"username\":\"alice\"}," +
                "{\"id\":\"uid-2\",\"username\":\"bob\"}]";
        HttpResponse<String> usersResp = mockResponse(200, usersJson);

        // Logout responses: 204 for first, 200 for second
        HttpResponse<String> logoutResp1 = mockResponse(204, "");
        HttpResponse<String> logoutResp2 = mockResponse(200, "");

        when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
                .thenReturn(tokenResp)
                .thenReturn(usersResp)
                .thenReturn(logoutResp1)
                .thenReturn(logoutResp2);

        int result = service.logoutAllSessions();

        assertThat(result).isEqualTo(2);
        // token + users + 2 logout calls = 4 total
        verify(mockHttpClient, times(4)).send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class));
    }

    @Test
    @DisplayName("logoutAllSessions — some logout calls fail (non 200/204) — partial count returned")
    void logoutAllSessions_partialLogout_returnsPartialCount() throws Exception {
        HttpResponse<String> tokenResp = mockResponse(200,
                "{\"access_token\":\"token\"}");

        String usersJson = "[{\"id\":\"uid-1\",\"username\":\"alice\"}," +
                "{\"id\":\"uid-2\",\"username\":\"bob\"}]";
        HttpResponse<String> usersResp = mockResponse(200, usersJson);

        // First succeeds (204), second fails (500)
        HttpResponse<String> logoutOk = mockResponse(204, "");
        HttpResponse<String> logoutFail = mockResponse(500, "error");

        when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
                .thenReturn(tokenResp)
                .thenReturn(usersResp)
                .thenReturn(logoutOk)
                .thenReturn(logoutFail);

        int result = service.logoutAllSessions();

        assertThat(result).isEqualTo(1);
    }

    @Test
    @DisplayName("logoutAllSessions — empty user list — returns 0")
    void logoutAllSessions_noUsers_returnsZero() throws Exception {
        HttpResponse<String> tokenResp = mockResponse(200,
                "{\"access_token\":\"token\"}");
        HttpResponse<String> usersResp = mockResponse(200, "[]");

        when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
                .thenReturn(tokenResp)
                .thenReturn(usersResp);

        int result = service.logoutAllSessions();

        assertThat(result).isEqualTo(0);
    }

    @Test
    @DisplayName("logoutAllSessions — user without username field — uses id as fallback")
    void logoutAllSessions_userWithoutUsernameField_usesIdAsFallback() throws Exception {
        HttpResponse<String> tokenResp = mockResponse(200,
                "{\"access_token\":\"token\"}");

        // user has no "username" field — only "id"
        String usersJson = "[{\"id\":\"uid-no-username\"}]";
        HttpResponse<String> usersResp = mockResponse(200, usersJson);
        HttpResponse<String> logoutResp = mockResponse(204, "");

        when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
                .thenReturn(tokenResp)
                .thenReturn(usersResp)
                .thenReturn(logoutResp);

        int result = service.logoutAllSessions();

        assertThat(result).isEqualTo(1);
    }

    // ── getAdminToken failure paths ────────────────────────────────────────────

    @Test
    @DisplayName("logoutAllSessions — token request fails with non-200 — throws RuntimeException")
    void logoutAllSessions_tokenRequestFails_throwsException() throws Exception {
        HttpResponse<String> tokenResp = mockResponse(401, "Unauthorized");

        when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
                .thenReturn(tokenResp);

        assertThatThrownBy(() -> service.logoutAllSessions())
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Échec authentification admin Keycloak")
                .hasMessageContaining("401");
    }

    @Test
    @DisplayName("logoutAllSessions — users list request fails — throws RuntimeException")
    void logoutAllSessions_usersRequestFails_throwsException() throws Exception {
        HttpResponse<String> tokenResp = mockResponse(200,
                "{\"access_token\":\"token\"}");
        HttpResponse<String> usersResp = mockResponse(403, "Forbidden");

        when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
                .thenReturn(tokenResp)
                .thenReturn(usersResp);

        assertThatThrownBy(() -> service.logoutAllSessions())
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("Impossible de lister les utilisateurs Keycloak")
                .hasMessageContaining("403");
    }

    @Test
    @DisplayName("logoutAllSessions — HttpClient throws IOException — propagates exception")
    void logoutAllSessions_ioException_propagates() throws Exception {
        when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
                .thenThrow(new java.io.IOException("Connection refused"));

        assertThatThrownBy(() -> service.logoutAllSessions())
                .isInstanceOf(Exception.class);
    }

    @Test
    @DisplayName("logoutAllSessions — special chars in password — URL-encodes correctly")
    void logoutAllSessions_specialCharsInPassword_urlEncoded() throws Exception {
        setField(service, "adminPassword", "p@ss w0rd+special=chars&more");

        HttpResponse<String> tokenResp = mockResponse(200,
                "{\"access_token\":\"token\"}");
        HttpResponse<String> usersResp = mockResponse(200, "[]");

        when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
                .thenReturn(tokenResp)
                .thenReturn(usersResp);

        // Should not throw — URL encoding is handled internally
        int result = service.logoutAllSessions();
        assertThat(result).isEqualTo(0);
    }

    @Test
    @DisplayName("logoutAllSessions — custom realm name used in URL")
    void logoutAllSessions_customRealm_usedInUrl() throws Exception {
        setField(service, "realm", "my-custom-realm");

        HttpResponse<String> tokenResp = mockResponse(200,
                "{\"access_token\":\"token\"}");
        HttpResponse<String> usersResp = mockResponse(200, "[]");

        when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
                .thenReturn(tokenResp)
                .thenReturn(usersResp);

        int result = service.logoutAllSessions();
        assertThat(result).isEqualTo(0);
    }
}
