package de.gecheckt.pdf.umbenenner.adapter.out.modelcatalog;

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

import java.net.ConnectException;
import java.net.UnknownHostException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.util.Optional;

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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogRequest;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult;

/**
 * Unit tests for {@link ClaudeModelCatalogAdapter}.
 * <p>
 * All tests inject a mock {@link HttpClient} via the package-private
 * {@code fetchAvailableModelsWithClient} method to avoid network access.
 */
@ExtendWith(MockitoExtension.class)
class ClaudeModelCatalogAdapterTest {

    private static final String API_KEY = "test-api-key";
    private static final String BASE_URL = "http://localhost:9999";
    private static final int TIMEOUT = 5;
    private static final String PROVIDER_ID = "claude";

    @Mock
    private HttpClient httpClient;

    @Mock
    private HttpResponse<String> httpResponse;

    private ClaudeModelCatalogAdapter adapter;

    @BeforeEach
    void setUp() {
        adapter = new ClaudeModelCatalogAdapter();
    }

    @Test
    @DisplayName("HTTP 200 with non-empty model list returns Success")
    void fetchModels_http200WithModels_returnsSuccess() throws Exception {
        String responseBody = """
                {"data":[{"id":"claude-3-opus","type":"model"},{"id":"claude-3-sonnet","type":"model"}]}
                """;
        doReturn(200).when(httpResponse).statusCode();
        doReturn(responseBody).when(httpResponse).body();
        doReturn(httpResponse).when(httpClient).send(any(HttpRequest.class), any());

        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of(API_KEY), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.Success.class);
        ModelCatalogResult.Success success = (ModelCatalogResult.Success) result;
        assertThat(success.models()).containsExactly("claude-3-opus", "claude-3-sonnet");
        assertThat(success.providerIdentifier()).isEqualTo(PROVIDER_ID);
    }

    @Test
    @DisplayName("HTTP 200 with empty data array returns EmptyList")
    void fetchModels_http200EmptyDataArray_returnsEmptyList() throws Exception {
        String responseBody = """
                {"data":[]}
                """;
        doReturn(200).when(httpResponse).statusCode();
        doReturn(responseBody).when(httpResponse).body();
        doReturn(httpResponse).when(httpClient).send(any(HttpRequest.class), any());

        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of(API_KEY), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.EmptyList.class);
    }

    @Test
    @DisplayName("HTTP 401 returns TechnicalFailure with AUTHENTICATION_FAILED")
    void fetchModels_http401_returnsAuthenticationFailed() throws Exception {
        doReturn(401).when(httpResponse).statusCode();
        doReturn(httpResponse).when(httpClient).send(any(HttpRequest.class), any());

        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of(API_KEY), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.TechnicalFailure.class);
        ModelCatalogResult.TechnicalFailure failure = (ModelCatalogResult.TechnicalFailure) result;
        assertThat(failure.errorCategory()).isEqualTo("AUTHENTICATION_FAILED");
    }

    @Test
    @DisplayName("HTTP 403 returns TechnicalFailure with AUTHENTICATION_FAILED")
    void fetchModels_http403_returnsAuthenticationFailed() throws Exception {
        doReturn(403).when(httpResponse).statusCode();
        doReturn(httpResponse).when(httpClient).send(any(HttpRequest.class), any());

        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of(API_KEY), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.TechnicalFailure.class);
        assertThat(((ModelCatalogResult.TechnicalFailure) result).errorCategory())
                .isEqualTo("AUTHENTICATION_FAILED");
    }

    @Test
    @DisplayName("HTTP 404 returns TechnicalFailure with ENDPOINT_NOT_FOUND")
    void fetchModels_http404_returnsEndpointNotFound() throws Exception {
        doReturn(404).when(httpResponse).statusCode();
        doReturn(httpResponse).when(httpClient).send(any(HttpRequest.class), any());

        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of(API_KEY), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.TechnicalFailure.class);
        assertThat(((ModelCatalogResult.TechnicalFailure) result).errorCategory())
                .isEqualTo("ENDPOINT_NOT_FOUND");
    }

    @Test
    @DisplayName("HTTP 500 returns TechnicalFailure with SERVER_ERROR")
    void fetchModels_http500_returnsServerError() throws Exception {
        doReturn(500).when(httpResponse).statusCode();
        doReturn(httpResponse).when(httpClient).send(any(HttpRequest.class), any());

        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of(API_KEY), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.TechnicalFailure.class);
        assertThat(((ModelCatalogResult.TechnicalFailure) result).errorCategory())
                .isEqualTo("SERVER_ERROR");
    }

    @Test
    @DisplayName("ConnectException returns TechnicalFailure with CONNECTION_FAILURE")
    void fetchModels_connectException_returnsConnectionFailure() throws Exception {
        org.mockito.Mockito.when(httpClient.send(any(HttpRequest.class), any()))
                .thenThrow(new ConnectException("connection refused"));

        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of(API_KEY), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.TechnicalFailure.class);
        assertThat(((ModelCatalogResult.TechnicalFailure) result).errorCategory())
                .isEqualTo("CONNECTION_FAILURE");
    }

    @Test
    @DisplayName("Unparseable JSON response returns TechnicalFailure with INVALID_RESPONSE")
    void fetchModels_invalidJson_returnsInvalidResponse() throws Exception {
        doReturn(200).when(httpResponse).statusCode();
        doReturn("this is not json at all!!!").when(httpResponse).body();
        doReturn(httpResponse).when(httpClient).send(any(HttpRequest.class), any());

        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of(API_KEY), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.TechnicalFailure.class);
        assertThat(((ModelCatalogResult.TechnicalFailure) result).errorCategory())
                .isEqualTo("INVALID_RESPONSE");
    }

    @Test
    @DisplayName("Missing API key returns IncompleteConfiguration")
    void fetchModels_missingApiKey_returnsIncompleteConfiguration() {
        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.empty(), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.IncompleteConfiguration.class);
        ModelCatalogResult.IncompleteConfiguration incomplete =
                (ModelCatalogResult.IncompleteConfiguration) result;
        assertThat(incomplete.missingReason()).containsIgnoringCase("API-Schlüssel");
    }

    @Test
    @DisplayName("Blank API key returns IncompleteConfiguration")
    void fetchModels_blankApiKey_returnsIncompleteConfiguration() {
        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of("   "), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.IncompleteConfiguration.class);
    }

    @Test
    @DisplayName("No base URL falls back to Anthropic default")
    void fetchModels_noBaseUrl_usesDefault() {
        // Verify that no exception is thrown when building the URI with no base URL.
        // The adapter should fall back to https://api.anthropic.com without crashing.
        // (We don't send a real HTTP request here; we just verify the adapter doesn't throw
        //  during the URI construction phase before the send call.)
        assertThat(ClaudeModelCatalogAdapter.DEFAULT_BASE_URL).isEqualTo("https://api.anthropic.com");
    }

    @Test
    @DisplayName("UnknownHostException returns TechnicalFailure with CONNECTION_FAILURE")
    void fetchModels_unknownHost_returnsConnectionFailure() throws Exception {
        org.mockito.Mockito.when(httpClient.send(any(HttpRequest.class), any()))
                .thenThrow(new UnknownHostException("unknown host"));

        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of(API_KEY), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.TechnicalFailure.class);
        assertThat(((ModelCatalogResult.TechnicalFailure) result).errorCategory())
                .isEqualTo("CONNECTION_FAILURE");
    }

    @Test
    @DisplayName("HttpTimeoutException returns TechnicalFailure with CONNECTION_FAILURE")
    void fetchModels_httpTimeout_returnsConnectionFailure() throws Exception {
        // The adapter groups HTTP timeouts together with other connection failures under
        // the CONNECTION_FAILURE category (no separate TIMEOUT category).
        org.mockito.Mockito.when(httpClient.send(any(HttpRequest.class), any()))
                .thenThrow(new HttpTimeoutException("request timed out"));

        ModelCatalogRequest request = new ModelCatalogRequest(PROVIDER_ID,
                Optional.of(BASE_URL), Optional.of(API_KEY), TIMEOUT);

        ModelCatalogResult result = adapter.fetchAvailableModelsWithClient(request, httpClient);

        assertThat(result).isInstanceOf(ModelCatalogResult.TechnicalFailure.class);
        assertThat(((ModelCatalogResult.TechnicalFailure) result).errorCategory())
                .isEqualTo("CONNECTION_FAILURE");
    }
}
