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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.junit.jupiter.api.Test;

/**
 * Unit-Tests für {@link CorrectionExecutionService}.
 * <p>
 * Prüft das Dispatch-Verhalten und die Kein-Frühabbruch-Semantik.
 */
class CorrectionExecutionServiceTest {

    // =========================================================================
    // No-op Port-Implementierungen für Tests
    // =========================================================================

    /** Port-Stub, der alle Aufrufe als Applied zurückgibt. */
    private static ResourceCreationPort allAppliedPort() {
        return new ResourceCreationPort() {
            @Override
            public CorrectionOutcome createDirectory(CorrectionSuggestion.CreateDirectory s) {
                return new CorrectionOutcome.Applied(s, "Ordner angelegt");
            }
            @Override
            public CorrectionOutcome createPromptFile(CorrectionSuggestion.CreatePromptFile s) {
                return new CorrectionOutcome.Applied(s, "Prompt erzeugt");
            }
            @Override
            public CorrectionOutcome prepareSqlitePath(CorrectionSuggestion.PrepareSqlitePath s) {
                return new CorrectionOutcome.Applied(s, "SQLite-Pfad vorbereitet");
            }
        };
    }

    /** Port-Stub, der createDirectory als Failed, den Rest als Applied zurückgibt. */
    private static ResourceCreationPort firstFailsPort() {
        return new ResourceCreationPort() {
            @Override
            public CorrectionOutcome createDirectory(CorrectionSuggestion.CreateDirectory s) {
                return new CorrectionOutcome.Failed(s, "Ordner nicht anlegbar");
            }
            @Override
            public CorrectionOutcome createPromptFile(CorrectionSuggestion.CreatePromptFile s) {
                return new CorrectionOutcome.Applied(s, "Prompt erzeugt");
            }
            @Override
            public CorrectionOutcome prepareSqlitePath(CorrectionSuggestion.PrepareSqlitePath s) {
                return new CorrectionOutcome.Applied(s, "SQLite-Pfad vorbereitet");
            }
        };
    }

    // =========================================================================
    // Tests
    // =========================================================================

    @Test
    void execute_emptyPlan_returnsEmptyReport() {
        CorrectionExecutionService service = new CorrectionExecutionService(allAppliedPort());

        CorrectionExecutionReport report = service.execute(CorrectionPlan.empty());

        assertNotNull(report);
        assertEquals(0, report.size());
        assertFalse(report.hasFailures());
        assertFalse(report.hasNotAttempted());
    }

    @Test
    void execute_planWithThreeSuggestions_allSucceed_reportHasThreeApplied() {
        CorrectionExecutionService service = new CorrectionExecutionService(allAppliedPort());

        CorrectionSuggestion.CreateDirectory dir =
                new CorrectionSuggestion.CreateDirectory("C:/foo", "Zielordner anlegen");
        CorrectionSuggestion.CreatePromptFile prompt =
                new CorrectionSuggestion.CreatePromptFile("C:/foo/prompt.txt", "Prompt anlegen", 60);
        CorrectionSuggestion.PrepareSqlitePath sqlite =
                new CorrectionSuggestion.PrepareSqlitePath("C:/foo/db.sqlite", "SQLite vorbereiten");

        CorrectionPlan plan = new CorrectionPlan(List.of(dir, prompt, sqlite));
        CorrectionExecutionReport report = service.execute(plan);

        assertEquals(3, report.size());
        assertTrue(report.allApplied(), "Alle drei Korrekturen sollen Applied sein");
        assertFalse(report.hasFailures());
    }

    @Test
    void execute_planWithOneFailing_othersStillExecuted_noEarlyAbort() {
        CorrectionExecutionService service = new CorrectionExecutionService(firstFailsPort());

        CorrectionSuggestion.CreateDirectory dir =
                new CorrectionSuggestion.CreateDirectory("C:/foo", "Ordner anlegen");
        CorrectionSuggestion.CreatePromptFile prompt =
                new CorrectionSuggestion.CreatePromptFile("C:/foo/prompt.txt", "Prompt anlegen", 60);
        CorrectionSuggestion.PrepareSqlitePath sqlite =
                new CorrectionSuggestion.PrepareSqlitePath("C:/foo/db.sqlite", "SQLite vorbereiten");

        CorrectionPlan plan = new CorrectionPlan(List.of(dir, prompt, sqlite));
        CorrectionExecutionReport report = service.execute(plan);

        assertEquals(3, report.size(), "Alle 3 Korrekturen müssen versucht worden sein (kein Frühabbruch)");
        assertTrue(report.hasFailures(), "Erste Korrektur ist fehlgeschlagen");
        assertFalse(report.allApplied());

        // Erster Eintrag: Failed (CreateDirectory)
        assertInstanceOf(CorrectionOutcome.Failed.class, report.outcomes().get(0));
        // Zweiter und dritter Eintrag: Applied (trotz Fehler im ersten)
        assertInstanceOf(CorrectionOutcome.Applied.class, report.outcomes().get(1));
        assertInstanceOf(CorrectionOutcome.Applied.class, report.outcomes().get(2));
    }

    @Test
    void constructor_nullPort_throwsNullPointerException() {
        assertThrows(NullPointerException.class,
                () -> new CorrectionExecutionService(null));
    }

    @Test
    void execute_nullPlan_throwsNullPointerException() {
        CorrectionExecutionService service = new CorrectionExecutionService(allAppliedPort());
        assertThrows(NullPointerException.class, () -> service.execute(null));
    }

    // Helper for instanceOf assertion
    private static <T> void assertInstanceOf(Class<T> expectedType, Object actual) {
        assertTrue(expectedType.isInstance(actual),
                "Expected instance of " + expectedType.getSimpleName()
                        + " but got " + (actual == null ? "null" : actual.getClass().getSimpleName()));
    }

    // =========================================================================
    // Tests: CreatePromptFile-Dispatch prüft DefaultPromptTemplate-Inhalt
    // =========================================================================

    /**
     * Der {@link CorrectionExecutionService} dispatcht {@link CorrectionSuggestion.CreatePromptFile}
     * an den Port. Ein Port-Stub, der den Inhalt der Suggestion zurückgibt, muss den
     * deutschen Standardinhalt aus {@link DefaultPromptTemplate#defaultContent(int)} enthalten,
     * wenn der Adapter ihn korrekt befüllt. Hier prüfen wir lediglich, dass
     * {@link DefaultPromptTemplate#defaultContent(int)} einen sinnvollen deutschen Text liefert,
     * der für die Dispatch-Kette geeignet ist.
     */
    @Test
    void createPromptFile_dispatch_defaultContentIsGermanAndNonEmpty() {
        // Der Dispatch selbst ist im Service zustandslos.
        // Wir prüfen hier, dass DefaultPromptTemplate den benötigten Inhalt liefert,
        // damit der Adapter ihn verwenden kann.
        String content = DefaultPromptTemplate.defaultContent(60);
        assertNotNull(content);
        assertFalse(content.isBlank(), "DefaultPromptTemplate.defaultContent(60) darf nicht leer sein");
        assertTrue(content.contains("Titel"), "Inhalt muss deutsches Schlüsselwort 'Titel' enthalten");
        assertTrue(content.contains("date"), "Inhalt muss JSON-Feld 'date' beschreiben");
        assertTrue(content.contains("reasoning"), "Inhalt muss JSON-Feld 'reasoning' beschreiben");
    }
}
