package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.Instant;
import java.util.List;

import org.junit.jupiter.api.Test;

import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;

/**
 * Unit-Tests für {@link ProviderTechnicalTestService}.
 * <p>
 * Testet alle relevanten Kombinationen aus {@link ModelCatalogResult}-Varianten und
 * API-Key-Herkunft für beide Provider-Familien. Port-Stubs werden als einfache Lambdas
 * implementiert.
 */
class ProviderTechnicalTestServiceTest {

    // ------------------------------------------------------------------ Hilfsmethoden

    private static EditorValidationInput claudeInput(EffectiveApiKeyDescriptor apiKeyDescriptor,
                                                      String model) {
        return new EditorValidationInput(
                "claude",
                "/src", "/tgt", "/db.sqlite", "/prompt.txt",
                "3", "10", "2000", "60",
                "https://api.anthropic.com", model, "30",
                apiKeyDescriptor, "sk-test",
                "https://api.openai.com", "gpt-4", "30",
                EffectiveApiKeyDescriptor.absent(), "");
    }

    private static EditorValidationInput openaiInput(EffectiveApiKeyDescriptor apiKeyDescriptor,
                                                       String model) {
        return new EditorValidationInput(
                "openai-compatible",
                "/src", "/tgt", "/db.sqlite", "/prompt.txt",
                "3", "10", "2000", "60",
                "https://api.anthropic.com", "claude-3-sonnet", "30",
                EffectiveApiKeyDescriptor.absent(), "",
                "https://api.openai.com", model, "30",
                apiKeyDescriptor, "sk-test");
    }

    private static EffectiveApiKeyDescriptor keyFromEnv() {
        return EffectiveApiKeyDescriptor.fromProviderEnvVar("ANTHROPIC_API_KEY");
    }

    private static EffectiveApiKeyDescriptor keyFromProperty() {
        return EffectiveApiKeyDescriptor.fromPropertyFile();
    }

    private static EffectiveApiKeyDescriptor keyAbsent() {
        return EffectiveApiKeyDescriptor.absent();
    }

    /**
     * Stub-Port der immer den über den Konstruktor übergebenen Descriptor zurückgibt,
     * unabhängig von family und propertyValue. Liefert den Roh-API-Schlüssel aus
     * {@code propertyValue}, falls gesetzt; sonst einen festen Stub-Wert, sobald der
     * Descriptor eine Quelle meldet.
     */
    private static de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort
            apiKeyPort(EffectiveApiKeyDescriptor descriptor) {
        return new de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort() {
            @Override
            public EffectiveApiKeyDescriptor resolve(
                    de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily family,
                    String propertyValue) {
                return descriptor;
            }

            @Override
            public java.util.Optional<String> resolveEffectiveApiKeyValue(
                    de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily family,
                    String propertyValue) {
                if (propertyValue != null && !propertyValue.isBlank()) {
                    return java.util.Optional.of(propertyValue);
                }
                return descriptor.isAbsent()
                        ? java.util.Optional.empty()
                        : java.util.Optional.of("stub-key");
            }
        };
    }

    /**
     * Stub-Port der immer das angegebene {@link ModelCatalogResult} zurückgibt.
     */
    private static de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort
            catalogPort(ModelCatalogResult result) {
        return request -> result;
    }

    // ------------------------------------------------------------------ Tests: API-Key absent

    @Test
    void givenApiKeyAbsent_allRemoteCheckpointsAreNotApplicable() {
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.IncompleteConfiguration("claude", "nicht erreichbar")),
                apiKeyPort(keyAbsent()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyAbsent(), "claude-3-sonnet"));

        assertThat(results).hasSize(5);
        assertThat(findById(results, CheckpointId.API_KEY_PRESENT))
                .isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) findById(results, CheckpointId.API_KEY_PRESENT)).severity())
                .isEqualTo(CheckpointSeverity.ERROR);
        assertThat(findById(results, CheckpointId.BASE_URL_REACHABLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
        assertThat(findById(results, CheckpointId.API_KEY_ACCEPTED))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
        assertThat(findById(results, CheckpointId.MODEL_LIST_AVAILABLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
        assertThat(findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
    }

    @Test
    void givenApiKeyAbsent_noModelCatalogCallIsMade() {
        boolean[] called = {false};
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                request -> { called[0] = true; return new ModelCatalogResult.EmptyList("claude", Instant.now()); },
                apiKeyPort(keyAbsent()));

        service.runProviderChecks(claudeInput(keyAbsent(), "claude-3-sonnet"));

        assertThat(called[0]).isFalse();
    }

    // ------------------------------------------------------------------ Tests: Success mit passendem Modell

    @Test
    void givenSuccessWithMatchingModel_allFiveCheckpointsSucceed() {
        List<String> models = List.of("claude-3-sonnet", "claude-3-opus");
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.Success("claude", models, Instant.now())),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyFromEnv(), "claude-3-sonnet"));

        assertThat(results).hasSize(5);
        assertThat(findById(results, CheckpointId.API_KEY_PRESENT)).isInstanceOf(CheckpointResult.Success.class);
        assertThat(findById(results, CheckpointId.BASE_URL_REACHABLE)).isInstanceOf(CheckpointResult.Success.class);
        assertThat(findById(results, CheckpointId.API_KEY_ACCEPTED)).isInstanceOf(CheckpointResult.Success.class);
        assertThat(findById(results, CheckpointId.MODEL_LIST_AVAILABLE)).isInstanceOf(CheckpointResult.Success.class);
        assertThat(findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE)).isInstanceOf(CheckpointResult.Success.class);
    }

    @Test
    void givenSuccessWithMatchingModel_apiKeyPresentMentionsEnvSource() {
        List<String> models = List.of("gpt-4");
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.Success("openai-compatible", models, Instant.now())),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                openaiInput(keyFromEnv(), "gpt-4"));

        CheckpointResult apiKeyResult = findById(results, CheckpointId.API_KEY_PRESENT);
        assertThat(apiKeyResult).isInstanceOf(CheckpointResult.Success.class);
        assertThat(((CheckpointResult.Success) apiKeyResult).message()).contains("Umgebungsvariable");
    }

    @Test
    void givenSuccessWithMatchingModel_apiKeyPresentMentionsPropertySource() {
        List<String> models = List.of("claude-3-opus");
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.Success("claude", models, Instant.now())),
                apiKeyPort(keyFromProperty()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyFromProperty(), "claude-3-opus"));

        CheckpointResult apiKeyResult = findById(results, CheckpointId.API_KEY_PRESENT);
        assertThat(apiKeyResult).isInstanceOf(CheckpointResult.Success.class);
        assertThat(((CheckpointResult.Success) apiKeyResult).message()).contains("Properties-Datei");
    }

    // ------------------------------------------------------------------ Tests: Success ohne passendes Modell

    @Test
    void givenSuccessWithoutMatchingModel_selectedModelPlausibleIsFailureWarning() {
        List<String> models = List.of("claude-3-opus", "claude-3-haiku");
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.Success("claude", models, Instant.now())),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyFromEnv(), "nonexistent-model"));

        CheckpointResult plausibleResult = findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE);
        assertThat(plausibleResult).isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) plausibleResult).severity()).isEqualTo(CheckpointSeverity.WARNING);
        assertThat(((CheckpointResult.Failure) plausibleResult).message()).contains("nonexistent-model");
    }

    @Test
    void givenSuccessWithBlankModel_selectedModelPlausibleIsFailureError() {
        List<String> models = List.of("claude-3-sonnet");
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.Success("claude", models, Instant.now())),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyFromEnv(), ""));

        CheckpointResult plausibleResult = findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE);
        assertThat(plausibleResult).isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) plausibleResult).severity()).isEqualTo(CheckpointSeverity.ERROR);
    }

    // ------------------------------------------------------------------ Tests: EmptyList

    @Test
    void givenEmptyList_modelListIsFailureWarning_andModelPlausibleIsNotApplicable() {
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.EmptyList("claude", Instant.now())),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyFromEnv(), "claude-3-sonnet"));

        assertThat(findById(results, CheckpointId.BASE_URL_REACHABLE)).isInstanceOf(CheckpointResult.Success.class);
        assertThat(findById(results, CheckpointId.API_KEY_ACCEPTED)).isInstanceOf(CheckpointResult.Success.class);
        CheckpointResult modelListResult = findById(results, CheckpointId.MODEL_LIST_AVAILABLE);
        assertThat(modelListResult).isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) modelListResult).severity()).isEqualTo(CheckpointSeverity.WARNING);
        assertThat(findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
    }

    // ------------------------------------------------------------------ Tests: IncompleteConfiguration

    @Test
    void givenIncompleteConfiguration_modelListIsFailureError_othersNotApplicable() {
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.IncompleteConfiguration("claude", "Base-URL fehlt")),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyFromEnv(), "claude-3-sonnet"));

        assertThat(findById(results, CheckpointId.BASE_URL_REACHABLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
        assertThat(findById(results, CheckpointId.API_KEY_ACCEPTED))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
        CheckpointResult modelListResult = findById(results, CheckpointId.MODEL_LIST_AVAILABLE);
        assertThat(modelListResult).isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) modelListResult).severity()).isEqualTo(CheckpointSeverity.ERROR);
        assertThat(((CheckpointResult.Failure) modelListResult).message()).contains("Base-URL fehlt");
        assertThat(findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
    }

    // ------------------------------------------------------------------ Tests: TechnicalFailure – AUTHENTICATION_FAILED

    @Test
    void givenTechnicalFailureAuthenticationFailed_apiKeyAcceptedIsFailureError_baseUrlIsSuccess() {
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.TechnicalFailure("claude",
                        "AUTHENTICATION_FAILED", "401 Unauthorized")),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyFromEnv(), "claude-3-sonnet"));

        assertThat(findById(results, CheckpointId.BASE_URL_REACHABLE)).isInstanceOf(CheckpointResult.Success.class);
        CheckpointResult apiKeyAcceptedResult = findById(results, CheckpointId.API_KEY_ACCEPTED);
        assertThat(apiKeyAcceptedResult).isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) apiKeyAcceptedResult).severity()).isEqualTo(CheckpointSeverity.ERROR);
        assertThat(((CheckpointResult.Failure) apiKeyAcceptedResult).message()).contains("401 Unauthorized");
        assertThat(findById(results, CheckpointId.MODEL_LIST_AVAILABLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
        assertThat(findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
    }

    // ------------------------------------------------------------------ Tests: TechnicalFailure – CONNECTION_FAILURE

    @Test
    void givenTechnicalFailureConnectionFailure_baseUrlIsFailureError_othersNotApplicable() {
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.TechnicalFailure("openai-compatible",
                        "CONNECTION_FAILURE", "Verbindung abgelehnt")),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                openaiInput(keyFromEnv(), "gpt-4"));

        CheckpointResult baseUrlResult = findById(results, CheckpointId.BASE_URL_REACHABLE);
        assertThat(baseUrlResult).isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) baseUrlResult).severity()).isEqualTo(CheckpointSeverity.ERROR);
        assertThat(((CheckpointResult.Failure) baseUrlResult).message()).contains("Verbindung abgelehnt");
        assertThat(findById(results, CheckpointId.API_KEY_ACCEPTED))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
        assertThat(findById(results, CheckpointId.MODEL_LIST_AVAILABLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
        assertThat(findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
    }

    @Test
    void givenTechnicalFailureEndpointNotFound_baseUrlIsFailureError() {
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.TechnicalFailure("claude",
                        "ENDPOINT_NOT_FOUND", "404 Not Found")),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyFromEnv(), "claude-3-sonnet"));

        CheckpointResult baseUrlResult = findById(results, CheckpointId.BASE_URL_REACHABLE);
        assertThat(baseUrlResult).isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) baseUrlResult).severity()).isEqualTo(CheckpointSeverity.ERROR);
        assertThat(((CheckpointResult.Failure) baseUrlResult).message()).contains("404 Not Found");
    }

    // ------------------------------------------------------------------ Tests: TechnicalFailure – SERVER_ERROR

    @Test
    void givenTechnicalFailureServerError_baseUrlIsSuccess_modelListIsFailureWarning() {
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.TechnicalFailure("claude",
                        "SERVER_ERROR", "500 Internal Server Error")),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyFromEnv(), "claude-3-sonnet"));

        assertThat(findById(results, CheckpointId.BASE_URL_REACHABLE)).isInstanceOf(CheckpointResult.Success.class);
        assertThat(findById(results, CheckpointId.API_KEY_ACCEPTED))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
        CheckpointResult modelListResult = findById(results, CheckpointId.MODEL_LIST_AVAILABLE);
        assertThat(modelListResult).isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) modelListResult).severity()).isEqualTo(CheckpointSeverity.WARNING);
        assertThat(((CheckpointResult.Failure) modelListResult).message()).contains("500 Internal Server Error");
        assertThat(findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
    }

    // ------------------------------------------------------------------ Tests: TechnicalFailure – INVALID_RESPONSE

    @Test
    void givenTechnicalFailureInvalidResponse_baseUrlIsSuccess_modelListIsFailureError() {
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.TechnicalFailure("openai-compatible",
                        "INVALID_RESPONSE", "Unerwartetes JSON-Format")),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                openaiInput(keyFromEnv(), "gpt-4o"));

        assertThat(findById(results, CheckpointId.BASE_URL_REACHABLE)).isInstanceOf(CheckpointResult.Success.class);
        assertThat(findById(results, CheckpointId.API_KEY_ACCEPTED))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
        CheckpointResult modelListResult = findById(results, CheckpointId.MODEL_LIST_AVAILABLE);
        assertThat(modelListResult).isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) modelListResult).severity()).isEqualTo(CheckpointSeverity.ERROR);
        assertThat(((CheckpointResult.Failure) modelListResult).message()).contains("Unerwartetes JSON-Format");
        assertThat(findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE))
                .isInstanceOf(CheckpointResult.NotApplicable.class);
    }

    // ------------------------------------------------------------------ Tests: TechnicalFailure – UNKNOWN

    @Test
    void givenTechnicalFailureUnknownCategory_allCheckpointsAreFailureError() {
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.TechnicalFailure("claude",
                        "UNKNOWN_MYSTERY_ERROR", "Seltsamer Fehler")),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                claudeInput(keyFromEnv(), "claude-3-sonnet"));

        // API_KEY_PRESENT bleibt Success (kein HTTP-Aufruf für diesen Check)
        assertThat(findById(results, CheckpointId.API_KEY_PRESENT)).isInstanceOf(CheckpointResult.Success.class);
        // Remote-Checks auf Failure ERROR
        assertThat(findById(results, CheckpointId.BASE_URL_REACHABLE))
                .isInstanceOf(CheckpointResult.Failure.class);
        assertThat(((CheckpointResult.Failure) findById(results, CheckpointId.BASE_URL_REACHABLE)).severity())
                .isEqualTo(CheckpointSeverity.ERROR);
        assertThat(findById(results, CheckpointId.API_KEY_ACCEPTED))
                .isInstanceOf(CheckpointResult.Failure.class);
        assertThat(findById(results, CheckpointId.MODEL_LIST_AVAILABLE))
                .isInstanceOf(CheckpointResult.Failure.class);
        assertThat(findById(results, CheckpointId.SELECTED_MODEL_PLAUSIBLE))
                .isInstanceOf(CheckpointResult.Failure.class);
    }

    // ------------------------------------------------------------------ Tests: Unbekannter Provider

    @Test
    void givenUnknownProviderIdentifier_allFiveCheckpointsAreFailureError() {
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.EmptyList("unknown", Instant.now())),
                apiKeyPort(keyFromEnv()));

        EditorValidationInput input = new EditorValidationInput(
                "unknown-provider",
                "/src", "/tgt", "/db.sqlite", "/prompt.txt",
                "3", "10", "2000", "60",
                "", "model", "30",
                EffectiveApiKeyDescriptor.absent(), "",
                "", "model", "30",
                EffectiveApiKeyDescriptor.absent(), "");

        List<CheckpointResult> results = service.runProviderChecks(input);

        assertThat(results).hasSize(5);
        results.forEach(r -> assertThat(r).isInstanceOf(CheckpointResult.Failure.class));
        results.forEach(r ->
                assertThat(((CheckpointResult.Failure) r).severity()).isEqualTo(CheckpointSeverity.ERROR));
    }

    // ------------------------------------------------------------------ Tests: OpenAI Provider

    @Test
    void givenOpenAiProvider_successWithMatchingModel_allFiveCheckpointsSucceed() {
        List<String> models = List.of("gpt-4", "gpt-3.5-turbo");
        ProviderTechnicalTestService service = new ProviderTechnicalTestService(
                catalogPort(new ModelCatalogResult.Success("openai-compatible", models, Instant.now())),
                apiKeyPort(keyFromEnv()));

        List<CheckpointResult> results = service.runProviderChecks(
                openaiInput(keyFromEnv(), "gpt-4"));

        assertThat(results).hasSize(5);
        results.forEach(r -> assertThat(r).isInstanceOf(CheckpointResult.Success.class));
    }

    // ------------------------------------------------------------------ Hilfsmethode

    private static CheckpointResult findById(List<CheckpointResult> results, CheckpointId id) {
        return results.stream()
                .filter(r -> r.checkpointId() == id)
                .findFirst()
                .orElseThrow(() -> new AssertionError("No result found for CheckpointId: " + id));
    }
}
