package de.gecheckt.pdf.umbenenner.domain.model;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

/**
 * Tests for document processing outcome types.
 * <p>
 * Verifies that all outcome types are properly created and validated.
 */
class DocumentProcessingOutcomeTest {

    @TempDir
    Path tempDir;

    private SourceDocumentCandidate candidate;

    @BeforeEach
    void setUp() throws Exception {
        Path pdfFile = tempDir.resolve("doc.pdf");
        Files.createFile(pdfFile);
        SourceDocumentLocator locator = new SourceDocumentLocator(pdfFile.toString());
        candidate = new SourceDocumentCandidate("doc.pdf", 1024L, locator);
    }


    @Test
    void testTechnicalDocumentError_ValidConstruction() {
        // Act
        var error = new TechnicalDocumentError(candidate, "I/O error", null);

        // Assert
        assertEquals(candidate, error.candidate());
        assertEquals("I/O error", error.errorMessage());
        assertNull(error.cause());
    }

    @Test
    void testTechnicalDocumentError_WithCause() {
        // Arrange
        var cause = new RuntimeException("File not found");

        // Act
        var error = new TechnicalDocumentError(candidate, "I/O error", cause);

        // Assert
        assertEquals(cause, error.cause());
    }

    @Test
    void testTechnicalDocumentError_WithNullCandidate_ThrowsException() {
        assertThrows(NullPointerException.class,
                () -> new TechnicalDocumentError(null, "Error", null));
    }

    @Test
    void testTechnicalDocumentError_WithNullErrorMessage_ThrowsException() {
        assertThrows(NullPointerException.class,
                () -> new TechnicalDocumentError(candidate, null, null));
    }

    @Test
    void testTechnicalDocumentError_WithEmptyErrorMessage_ThrowsException() {
        assertThrows(IllegalArgumentException.class,
                () -> new TechnicalDocumentError(candidate, "", null));
    }

    @Test
    void testTechnicalDocumentError_IsDocumentProcessingOutcome() {
        // Verify type relationship
        var error = new TechnicalDocumentError(candidate, "Error", null);
        assertInstanceOf(DocumentProcessingOutcome.class, error);
    }

    @Test
    void testPreCheckPassed_IsDocumentProcessingOutcome() {
        // Verify type relationship
        var extraction = new PdfExtractionSuccess("text", new PdfPageCount(1));
        var passed = new PreCheckPassed(candidate, extraction);
        assertInstanceOf(DocumentProcessingOutcome.class, passed);
    }

    @Test
    void testPreCheckFailed_IsDocumentProcessingOutcome() {
        // Verify type relationship
        var failed = new PreCheckFailed(candidate, PreCheckFailureReason.NO_USABLE_TEXT);
        assertInstanceOf(DocumentProcessingOutcome.class, failed);
    }

    @Test
    void testAllOutcomesAreExhaustive() {
        // This test verifies that the outcome types are the only implementations
        var extraction = new PdfExtractionSuccess("text", new PdfPageCount(1));

        DocumentProcessingOutcome[] outcomes = {
                new PreCheckPassed(candidate, extraction),
                new PreCheckFailed(candidate, PreCheckFailureReason.NO_USABLE_TEXT),
                new TechnicalDocumentError(candidate, "Technical extraction error", null)
        };

        for (DocumentProcessingOutcome outcome : outcomes) {
            assertInstanceOf(DocumentProcessingOutcome.class, outcome);
        }
    }

    // -------------------------------------------------------------------------
    // AiAttemptContext
    // -------------------------------------------------------------------------

    @Test
    void aiAttemptContext_validConstruction_fieldsAreSet() {
        var ctx = new AiAttemptContext("gpt-4", "prompt-v1", 2, 500, "{\"title\":\"Test\"}");

        assertEquals("gpt-4", ctx.modelName());
        assertEquals("prompt-v1", ctx.promptIdentifier());
        assertEquals(2, ctx.processedPageCount());
        assertEquals(500, ctx.sentCharacterCount());
        assertEquals("{\"title\":\"Test\"}", ctx.aiRawResponse());
    }

    @Test
    void aiAttemptContext_nullRawResponse_isAllowed() {
        var ctx = new AiAttemptContext("gpt-4", "prompt-v1", 1, 0, null);
        assertNull(ctx.aiRawResponse());
    }

    @Test
    void aiAttemptContext_nullModelName_throwsNPE() {
        assertThrows(NullPointerException.class,
                () -> new AiAttemptContext(null, "prompt-v1", 1, 0, null));
    }

    @Test
    void aiAttemptContext_nullPromptIdentifier_throwsNPE() {
        assertThrows(NullPointerException.class,
                () -> new AiAttemptContext("gpt-4", null, 1, 0, null));
    }

    @Test
    void aiAttemptContext_zeroPageCount_throwsIllegalArgument() {
        assertThrows(IllegalArgumentException.class,
                () -> new AiAttemptContext("gpt-4", "prompt-v1", 0, 0, null));
    }

    @Test
    void aiAttemptContext_negativeCharCount_throwsIllegalArgument() {
        assertThrows(IllegalArgumentException.class,
                () -> new AiAttemptContext("gpt-4", "prompt-v1", 1, -1, null));
    }

    // -------------------------------------------------------------------------
    // AiFunctionalFailure
    // -------------------------------------------------------------------------

    @Test
    void aiFunctionalFailure_validConstruction_fieldsAreSet() {
        var ctx = new AiAttemptContext("gpt-4", "prompt-v1", 1, 100, "{}");
        var failure = new AiFunctionalFailure(candidate, "Title too long", ctx);

        assertEquals(candidate, failure.candidate());
        assertEquals("Title too long", failure.errorMessage());
        assertEquals(ctx, failure.aiContext());
        assertInstanceOf(DocumentProcessingOutcome.class, failure);
    }

    @Test
    void aiFunctionalFailure_nullCandidate_throwsNPE() {
        var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
        assertThrows(NullPointerException.class,
                () -> new AiFunctionalFailure(null, "error", ctx));
    }

    @Test
    void aiFunctionalFailure_nullErrorMessage_throwsNPE() {
        var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
        assertThrows(NullPointerException.class,
                () -> new AiFunctionalFailure(candidate, null, ctx));
    }

    @Test
    void aiFunctionalFailure_nullAiContext_throwsNPE() {
        assertThrows(NullPointerException.class,
                () -> new AiFunctionalFailure(candidate, "error", null));
    }

    // -------------------------------------------------------------------------
    // AiTechnicalFailure
    // -------------------------------------------------------------------------

    @Test
    void aiTechnicalFailure_validConstruction_fieldsAreSet() {
        var ctx = new AiAttemptContext("gpt-4", "prompt-v1", 1, 100, null);
        var cause = new RuntimeException("timeout");
        var failure = new AiTechnicalFailure(candidate, "HTTP timeout", cause, ctx);

        assertEquals(candidate, failure.candidate());
        assertEquals("HTTP timeout", failure.errorMessage());
        assertEquals(cause, failure.cause());
        assertEquals(ctx, failure.aiContext());
        assertInstanceOf(DocumentProcessingOutcome.class, failure);
    }

    @Test
    void aiTechnicalFailure_nullCause_isAllowed() {
        var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
        var failure = new AiTechnicalFailure(candidate, "error", null, ctx);
        assertNull(failure.cause());
    }

    @Test
    void aiTechnicalFailure_nullCandidate_throwsNPE() {
        var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
        assertThrows(NullPointerException.class,
                () -> new AiTechnicalFailure(null, "error", null, ctx));
    }

    @Test
    void aiTechnicalFailure_nullErrorMessage_throwsNPE() {
        var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
        assertThrows(NullPointerException.class,
                () -> new AiTechnicalFailure(candidate, null, null, ctx));
    }

    @Test
    void aiTechnicalFailure_nullAiContext_throwsNPE() {
        assertThrows(NullPointerException.class,
                () -> new AiTechnicalFailure(candidate, "error", null, null));
    }

    // -------------------------------------------------------------------------
    // NamingProposalReady
    // -------------------------------------------------------------------------

    @Test
    void namingProposalReady_validConstruction_fieldsAreSet() {
        var ctx = new AiAttemptContext("gpt-4", "prompt-v1", 1, 100, "{\"title\":\"Rechnung\"}");
        var proposal = new NamingProposal(LocalDate.of(2026, 1, 15), DateSource.AI_PROVIDED, "Rechnung", "AI reasoning");
        var ready = new NamingProposalReady(candidate, proposal, ctx);

        assertEquals(candidate, ready.candidate());
        assertEquals(proposal, ready.proposal());
        assertEquals(ctx, ready.aiContext());
        assertInstanceOf(DocumentProcessingOutcome.class, ready);
    }

    @Test
    void namingProposalReady_nullCandidate_throwsNPE() {
        var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
        var proposal = new NamingProposal(LocalDate.now(), DateSource.AI_PROVIDED, "Test", "r");
        assertThrows(NullPointerException.class,
                () -> new NamingProposalReady(null, proposal, ctx));
    }

    @Test
    void namingProposalReady_nullProposal_throwsNPE() {
        var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
        assertThrows(NullPointerException.class,
                () -> new NamingProposalReady(candidate, null, ctx));
    }

    @Test
    void namingProposalReady_nullAiContext_throwsNPE() {
        var proposal = new NamingProposal(LocalDate.now(), DateSource.AI_PROVIDED, "Test", "r");
        assertThrows(NullPointerException.class,
                () -> new NamingProposalReady(candidate, proposal, null));
    }
}