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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.HashSet;
import java.util.Set;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionTechnicalError;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;

/**
 * Tests for {@link PdfTextExtractionPortAdapter}.
 * <p>
 * Validates basic extraction functionality and technical error handling.
 * All extraction problems are treated as {@link de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionTechnicalError},
 * not content errors. Content usability (text quality assessment) is handled in the application layer.
 * PDFs are created programmatically using PDFBox to avoid external dependencies on test files.
 */
class PdfTextExtractionPortAdapterTest {

    private PdfTextExtractionPortAdapter adapter;

    @TempDir
    Path tempDir;

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

    @Test
    void testNullCandidateThrowsNullPointerException() {
        assertThrows(NullPointerException.class, () -> adapter.extractTextAndPageCount(null));
    }

    @Test
    void testNonExistentFileReturnsTechnicalError() throws Exception {
        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
            "nonexistent.pdf",
            1,
            new SourceDocumentLocator("/path/that/does/not/exist.pdf")
        );

        PdfExtractionResult result = adapter.extractTextAndPageCount(candidate);

        assertInstanceOf(PdfExtractionTechnicalError.class, result);
        PdfExtractionTechnicalError error = (PdfExtractionTechnicalError) result;
        assertTrue(error.errorMessage().contains("not found"));
    }

    @Test
    void testUnreadableFileReturnsTechnicalError() throws Exception {
        // Create a PDF file
        Path pdfFile = tempDir.resolve("unreadable.pdf");
        createSimplePdf(pdfFile);
        
        // Make file unreadable (Unix-like systems)
        try {
            Set<PosixFilePermission> perms = new HashSet<>();
            Files.setPosixFilePermissions(pdfFile, perms);
        } catch (UnsupportedOperationException e) {
            // On Windows, we can't easily make a file unreadable while keeping it existing
            // So we'll skip this test on Windows by just returning early
            return;
        }

        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
            "unreadable.pdf",
            Files.size(pdfFile),
            new SourceDocumentLocator(pdfFile.toAbsolutePath().toString())
        );

        PdfExtractionResult result = adapter.extractTextAndPageCount(candidate);

        assertInstanceOf(PdfExtractionTechnicalError.class, result);
        PdfExtractionTechnicalError error = (PdfExtractionTechnicalError) result;
        assertTrue(error.errorMessage().contains("not readable"));
    }

    @Test
    void testZeroPagePdfReturnsTechnicalError() throws Exception {
        // Create a PDF with zero pages
        Path pdfFile = tempDir.resolve("zero-pages.pdf");
        createZeroPagePdf(pdfFile);

        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
            "zero-pages.pdf",
            Files.size(pdfFile),
            new SourceDocumentLocator(pdfFile.toAbsolutePath().toString())
        );

        PdfExtractionResult result = adapter.extractTextAndPageCount(candidate);

        assertInstanceOf(PdfExtractionTechnicalError.class, result);
        PdfExtractionTechnicalError error = (PdfExtractionTechnicalError) result;
        assertTrue(error.errorMessage().contains("zero pages"));
    }

    @Test
    void testSimplePdfExtractionSuccess() throws Exception {
        // Create a simple single-page PDF
        Path pdfFile = tempDir.resolve("simple.pdf");
        createSimplePdf(pdfFile);

        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
            "simple.pdf",
            Files.size(pdfFile),
            new SourceDocumentLocator(pdfFile.toAbsolutePath().toString())
        );

        PdfExtractionResult result = adapter.extractTextAndPageCount(candidate);

        assertInstanceOf(PdfExtractionSuccess.class, result);
        PdfExtractionSuccess success = (PdfExtractionSuccess) result;
        assertEquals(1, success.pageCount().value());
        assertNotNull(success.extractedText());
    }

    @Test
    void testMultiPagePdfExtractionSuccess() throws Exception {
        // Create a three-page PDF
        Path pdfFile = tempDir.resolve("multipage.pdf");
        createMultiPagePdf(pdfFile, 3);

        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
            "multipage.pdf",
            Files.size(pdfFile),
            new SourceDocumentLocator(pdfFile.toAbsolutePath().toString())
        );

        PdfExtractionResult result = adapter.extractTextAndPageCount(candidate);

        assertInstanceOf(PdfExtractionSuccess.class, result);
        PdfExtractionSuccess success = (PdfExtractionSuccess) result;
        assertEquals(3, success.pageCount().value());
        assertNotNull(success.extractedText());
    }

    @Test
    void testReadablePdfWithEmptyTextReturnsSuccess() throws Exception {
        // Create a PDF with no text content (blank page)
        // This is a technically readable PDF, so it should succeed
        Path pdfFile = tempDir.resolve("blank.pdf");
        createBlankPdf(pdfFile);

        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
            "blank.pdf",
            Files.size(pdfFile),
            new SourceDocumentLocator(pdfFile.toAbsolutePath().toString())
        );

        PdfExtractionResult result = adapter.extractTextAndPageCount(candidate);

        // Empty text is SUCCESS at extraction level, not an error
        // Fachliche Bewertung of text content happens in the application layer
        assertInstanceOf(PdfExtractionSuccess.class, result);
        PdfExtractionSuccess success = (PdfExtractionSuccess) result;
        assertEquals(1, success.pageCount().value());
        assertNotNull(success.extractedText());  // May be empty, but not null
    }

    @Test
    void testCorruptedPdfReturnsTechnicalError() throws Exception {
        // Create a file with binary garbage content – exists on disk but is not a valid PDF
        Path corruptFile = tempDir.resolve("corrupt.pdf");
        Files.write(corruptFile, "THIS IS NOT A PDF - RANDOM GARBAGE XYZ123!@#".getBytes());

        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
            "corrupt.pdf",
            Files.size(corruptFile),
            new SourceDocumentLocator(corruptFile.toAbsolutePath().toString())
        );

        PdfExtractionResult result = adapter.extractTextAndPageCount(candidate);

        // PDFBox cannot load a garbage file; the adapter must catch this and return TechnicalError
        assertInstanceOf(PdfExtractionTechnicalError.class, result,
            "Binary garbage file (not a real PDF) must yield TechnicalError, not Success");
        PdfExtractionTechnicalError error = (PdfExtractionTechnicalError) result;
        assertNotNull(error.errorMessage(), "TechnicalError must carry a non-null error message");
        assertFalse(error.errorMessage().isBlank(), "TechnicalError message must not be blank");
    }

    @Test
    void testExtractingLargePdfReturnsSuccess() throws Exception {
        // Create a large PDF with many pages
        Path largePdfFile = tempDir.resolve("large.pdf");
        createMultiPagePdf(largePdfFile, 50);

        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
            "large.pdf",
            Files.size(largePdfFile),
            new SourceDocumentLocator(largePdfFile.toAbsolutePath().toString())
        );

        PdfExtractionResult result = adapter.extractTextAndPageCount(candidate);

        assertInstanceOf(PdfExtractionSuccess.class, result);
        PdfExtractionSuccess success = (PdfExtractionSuccess) result;
        assertEquals(50, success.pageCount().value());
        assertNotNull(success.extractedText());
    }

    @Test
    void testPartiallyCorruptedPdfStillReturnsError() throws Exception {
        // Create a file that starts like PDF but has corrupted content
        Path partialCorruptFile = tempDir.resolve("partial-corrupt.pdf");
        byte[] pdfSignature = new byte[] {0x25, 0x50, 0x44, 0x46};  // %PDF in hex
        byte[] corruptData = "This is corrupted PDF content that will fail parsing".getBytes();
        byte[] combined = new byte[pdfSignature.length + corruptData.length];
        System.arraycopy(pdfSignature, 0, combined, 0, pdfSignature.length);
        System.arraycopy(corruptData, 0, combined, pdfSignature.length, corruptData.length);
        Files.write(partialCorruptFile, combined);

        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
            "partial-corrupt.pdf",
            Files.size(partialCorruptFile),
            new SourceDocumentLocator(partialCorruptFile.toAbsolutePath().toString())
        );

        PdfExtractionResult result = adapter.extractTextAndPageCount(candidate);

        assertInstanceOf(PdfExtractionTechnicalError.class, result);
        PdfExtractionTechnicalError error = (PdfExtractionTechnicalError) result;
        assertNotNull(error.errorMessage());
    }

    // --- Helper methods to create test PDFs ---

    /**
     * Creates a simple single-page PDF.
     */
    private void createSimplePdf(Path filePath) throws Exception {
        PDDocument document = new PDDocument();
        PDPage page = new PDPage();
        document.addPage(page);
        document.save(filePath.toAbsolutePath().toString());
        document.close();
    }

    /**
     * Creates a PDF with multiple blank pages.
     */
    private void createMultiPagePdf(Path filePath, int pageCount) throws Exception {
        PDDocument document = new PDDocument();
        for (int i = 0; i < pageCount; i++) {
            PDPage page = new PDPage();
            document.addPage(page);
        }
        document.save(filePath.toAbsolutePath().toString());
        document.close();
    }

    /**
     * Creates a blank PDF with a single page and no text.
     */
    private void createBlankPdf(Path filePath) throws Exception {
        PDDocument document = new PDDocument();
        PDPage page = new PDPage();
        document.addPage(page);
        document.save(filePath.toAbsolutePath().toString());
        document.close();
    }
    
    /**
     * Creates a PDF with zero pages.
     */
    private void createZeroPagePdf(Path filePath) throws Exception {
        PDDocument document = new PDDocument();
        // Add no pages
        document.save(filePath.toAbsolutePath().toString());
        document.close();
    }
}