package de.gecheckt.pdf.umbenenner.bootstrap.adapter;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import java.time.Instant;
import java.util.List;
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.AiModelCatalogPort;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogRequest;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult;

/**
 * Unit tests for {@link AiModelCatalogDispatcher}.
 * <p>
 * Verifies that the dispatcher correctly routes requests to the matching concrete
 * adapter based on the provider identifier in the request, and that unknown providers
 * are handled as {@link ModelCatalogResult.IncompleteConfiguration} without calling
 * either adapter.
 */
@ExtendWith(MockitoExtension.class)
class AiModelCatalogDispatcherTest {

    @Mock
    private AiModelCatalogPort claudeAdapter;

    @Mock
    private AiModelCatalogPort openAiCompatibleAdapter;

    private AiModelCatalogDispatcher dispatcher;

    @BeforeEach
    void setUp() {
        dispatcher = new AiModelCatalogDispatcher(claudeAdapter, openAiCompatibleAdapter);
    }

    @Test
    @DisplayName("Request with provider 'claude' is routed to the Claude adapter")
    void dispatch_claudeProvider_routesToClaudeAdapter() {
        ModelCatalogRequest request = new ModelCatalogRequest(
                "claude", Optional.empty(), Optional.of("key"), 5);
        ModelCatalogResult expected = new ModelCatalogResult.Success(
                "claude", List.of("claude-3-opus"), Instant.now());
        when(claudeAdapter.fetchAvailableModels(request)).thenReturn(expected);

        ModelCatalogResult result = dispatcher.fetchAvailableModels(request);

        assertThat(result).isSameAs(expected);
        verify(claudeAdapter).fetchAvailableModels(request);
        verifyNoInteractions(openAiCompatibleAdapter);
    }

    @Test
    @DisplayName("Request with provider 'openai-compatible' is routed to the OpenAI-compatible adapter")
    void dispatch_openAiCompatibleProvider_routesToOpenAiAdapter() {
        ModelCatalogRequest request = new ModelCatalogRequest(
                "openai-compatible", Optional.empty(), Optional.of("key"), 5);
        ModelCatalogResult expected = new ModelCatalogResult.Success(
                "openai-compatible", List.of("gpt-4o"), Instant.now());
        when(openAiCompatibleAdapter.fetchAvailableModels(request)).thenReturn(expected);

        ModelCatalogResult result = dispatcher.fetchAvailableModels(request);

        assertThat(result).isSameAs(expected);
        verify(openAiCompatibleAdapter).fetchAvailableModels(request);
        verifyNoInteractions(claudeAdapter);
    }

    @Test
    @DisplayName("Unknown provider identifier returns IncompleteConfiguration without calling any adapter")
    void dispatch_unknownProvider_returnsIncompleteConfigurationWithoutCallingAdapters() {
        ModelCatalogRequest request = new ModelCatalogRequest(
                "unknown-provider", Optional.empty(), Optional.of("key"), 5);

        ModelCatalogResult result = dispatcher.fetchAvailableModels(request);

        assertThat(result).isInstanceOf(ModelCatalogResult.IncompleteConfiguration.class);
        ModelCatalogResult.IncompleteConfiguration incomplete =
                (ModelCatalogResult.IncompleteConfiguration) result;
        assertThat(incomplete.providerIdentifier()).isEqualTo("unknown-provider");
        assertThat(incomplete.missingReason()).containsIgnoringCase("Unbekannter Provider");
        verifyNoInteractions(claudeAdapter);
        verifyNoInteractions(openAiCompatibleAdapter);
    }

    @Test
    @DisplayName("Null request throws NullPointerException")
    void dispatch_nullRequest_throwsNullPointerException() {
        assertThatNullPointerException()
                .isThrownBy(() -> dispatcher.fetchAvailableModels(null));
    }

    @Test
    @DisplayName("Null Claude adapter throws NullPointerException at construction")
    void constructor_nullClaudeAdapter_throwsNullPointerException() {
        assertThatNullPointerException()
                .isThrownBy(() -> new AiModelCatalogDispatcher(null, openAiCompatibleAdapter));
    }

    @Test
    @DisplayName("Null OpenAI-compatible adapter throws NullPointerException at construction")
    void constructor_nullOpenAiAdapter_throwsNullPointerException() {
        assertThatNullPointerException()
                .isThrownBy(() -> new AiModelCatalogDispatcher(claudeAdapter, null));
    }

    @Test
    @DisplayName("Dispatcher propagates adapter result unchanged for claude")
    void dispatch_claudeAdapter_propagatesResultUnchanged() {
        ModelCatalogRequest request = new ModelCatalogRequest(
                "claude", Optional.empty(), Optional.of("k"), 10);
        ModelCatalogResult.TechnicalFailure failure = new ModelCatalogResult.TechnicalFailure(
                "claude", "AUTHENTICATION_FAILED", "Bad credentials");
        when(claudeAdapter.fetchAvailableModels(request)).thenReturn(failure);

        ModelCatalogResult result = dispatcher.fetchAvailableModels(request);

        assertThat(result).isSameAs(failure);
    }

    @Test
    @DisplayName("Dispatcher propagates adapter result unchanged for openai-compatible")
    void dispatch_openAiCompatibleAdapter_propagatesResultUnchanged() {
        ModelCatalogRequest request = new ModelCatalogRequest(
                "openai-compatible", Optional.empty(), Optional.of("k"), 10);
        ModelCatalogResult.EmptyList emptyList = new ModelCatalogResult.EmptyList(
                "openai-compatible", Instant.now());
        when(openAiCompatibleAdapter.fetchAvailableModels(request)).thenReturn(emptyList);

        ModelCatalogResult result = dispatcher.fetchAvailableModels(request);

        assertThat(result).isSameAs(emptyList);
    }
}
