package com.storycove.service;

import com.storycove.entity.Author;
import com.storycove.entity.Collection;
import com.storycove.entity.Story;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * Tests for ImageService.
 * Note: Some tests use mocking due to filesystem and network dependencies.
 * Full integration tests would be in a separate test class.
 */
@ExtendWith(MockitoExtension.class)
class ImageServiceTest {

    @Mock
    private LibraryService libraryService;

    @Mock
    private StoryService storyService;

    @Mock
    private AuthorService authorService;

    @Mock
    private CollectionService collectionService;

    @InjectMocks
    private ImageService imageService;

    @TempDir
    Path tempDir;

    private MultipartFile validImageFile;
    private UUID testStoryId;

    @BeforeEach
    void setUp() throws IOException {
        testStoryId = UUID.randomUUID();

        // Create a simple valid PNG file (1x1 pixel)
        byte[] pngData = createMinimalPngData();
        validImageFile = new MockMultipartFile(
            "image", "test.png", "image/png", pngData
        );

        // Configure ImageService with test values — lenient to avoid UnnecessaryStubbingException
        // in tests that only use one of the two stubs
        lenient().when(libraryService.getCurrentImagePath()).thenReturn("/default");
        lenient().when(libraryService.getCurrentLibraryId()).thenReturn("default");

        // Set image service properties using reflection
        ReflectionTestUtils.setField(imageService, "baseUploadDir", tempDir.toString());
        ReflectionTestUtils.setField(imageService, "coverMaxWidth", 800);
        ReflectionTestUtils.setField(imageService, "coverMaxHeight", 1200);
        ReflectionTestUtils.setField(imageService, "avatarMaxSize", 400);
        ReflectionTestUtils.setField(imageService, "maxFileSize", 5242880L);
        ReflectionTestUtils.setField(imageService, "publicUrl", "http://localhost:6925");
    }

    // ========================================
    // File Validation Tests
    // ========================================

    @Test
    @DisplayName("Should reject null file")
    void testRejectNullFile() {
        // Act & Assert
        assertThrows(IllegalArgumentException.class, () -> {
            imageService.uploadImage(null, ImageService.ImageType.COVER);
        });
    }

    @Test
    @DisplayName("Should reject empty file")
    void testRejectEmptyFile() {
        // Arrange
        MockMultipartFile emptyFile = new MockMultipartFile(
            "image", "test.png", "image/png", new byte[0]
        );

        // Act & Assert
        assertThrows(IllegalArgumentException.class, () -> {
            imageService.uploadImage(emptyFile, ImageService.ImageType.COVER);
        });
    }

    @Test
    @DisplayName("Should reject file with invalid content type")
    void testRejectInvalidContentType() {
        // Arrange
        MockMultipartFile invalidFile = new MockMultipartFile(
            "image", "test.pdf", "application/pdf", "fake pdf content".getBytes()
        );

        // Act & Assert
        assertThrows(IllegalArgumentException.class, () -> {
            imageService.uploadImage(invalidFile, ImageService.ImageType.COVER);
        });
    }

    @Test
    @DisplayName("Should reject file with invalid extension")
    void testRejectInvalidExtension() {
        // Arrange
        MockMultipartFile invalidFile = new MockMultipartFile(
            "image", "test.gif", "image/png", createMinimalPngData()
        );

        // Act & Assert
        assertThrows(IllegalArgumentException.class, () -> {
            imageService.uploadImage(invalidFile, ImageService.ImageType.COVER);
        });
    }

    @Test
    @DisplayName("Should reject file exceeding size limit")
    void testRejectOversizedFile() {
        // Arrange
        // Create file larger than 5MB limit
        byte[] largeData = new byte[6 * 1024 * 1024]; // 6MB
        MockMultipartFile largeFile = new MockMultipartFile(
            "image", "large.png", "image/png", largeData
        );

        // Act & Assert
        assertThrows(IllegalArgumentException.class, () -> {
            imageService.uploadImage(largeFile, ImageService.ImageType.COVER);
        });
    }

    @Test
    @DisplayName("Should accept JPG files")
    void testAcceptJpgFile() {
        // Arrange
        MockMultipartFile jpgFile = new MockMultipartFile(
            "image", "test.jpg", "image/jpeg", createMinimalPngData() // Using PNG data for test simplicity
        );

        // Note: This test will fail at image processing stage since we're not providing real JPG data
        // but it validates that JPG is accepted as a file type
    }

    @Test
    @DisplayName("Should accept PNG files")
    void testAcceptPngFile() {
        // PNG is tested in setUp, this validates the behavior
        assertNotNull(validImageFile);
        assertEquals("image/png", validImageFile.getContentType());
    }

    @Test
    @DisplayName("Should accept WebP files")
    void testAcceptWebpFile() throws IOException {
        // Arrange
        MockMultipartFile webpFile = new MockMultipartFile(
            "image", "test.webp", "image/webp", createMinimalWebpData()
        );

        // Act & Assert — uploadContentImage saves raw bytes, so a valid WebP should be accepted and saved
        String resultPath = imageService.uploadContentImage(webpFile, testStoryId);
        assertTrue(resultPath.endsWith(".webp"), "Result path should end with .webp but was: " + resultPath);
        Path savedFile = tempDir.resolve("default").resolve(resultPath);
        assertTrue(Files.exists(savedFile), "WebP file should be saved on disk");
    }

    @Test
    @DisplayName("uploadContentImage should save WebP file without ImageIO processing")
    void uploadContentImage_acceptsWebpFile() throws IOException {
        // Arrange
        byte[] webpData = createMinimalWebpData();
        MockMultipartFile webpFile = new MockMultipartFile(
            "image", "content.webp", "image/webp", webpData
        );

        // Act
        String resultPath = imageService.uploadContentImage(webpFile, testStoryId);

        // Assert
        assertTrue(resultPath.startsWith("content/" + testStoryId + "/"),
            "Path should be content/{storyId}/filename but was: " + resultPath);
        assertTrue(resultPath.endsWith(".webp"), "Filename should retain .webp extension");
        Path savedFile = tempDir.resolve("default").resolve(resultPath);
        assertTrue(Files.exists(savedFile), "Saved file should exist on disk");
        assertArrayEquals(webpData, Files.readAllBytes(savedFile), "Saved bytes should match original WebP data");
    }

    // ========================================
    // Image Type Tests
    // ========================================

    @Test
    @DisplayName("Should have correct directory for COVER type")
    void testCoverImageDirectory() {
        assertEquals("covers", ImageService.ImageType.COVER.getDirectory());
    }

    @Test
    @DisplayName("Should have correct directory for AVATAR type")
    void testAvatarImageDirectory() {
        assertEquals("avatars", ImageService.ImageType.AVATAR.getDirectory());
    }

    @Test
    @DisplayName("Should have correct directory for CONTENT type")
    void testContentImageDirectory() {
        assertEquals("content", ImageService.ImageType.CONTENT.getDirectory());
    }

    // ========================================
    // Image Existence Tests
    // ========================================

    @Test
    @DisplayName("Should return false for null image path")
    void testImageExistsWithNullPath() {
        assertFalse(imageService.imageExists(null));
    }

    @Test
    @DisplayName("Should return false for empty image path")
    void testImageExistsWithEmptyPath() {
        assertFalse(imageService.imageExists(""));
        assertFalse(imageService.imageExists("   "));
    }

    @Test
    @DisplayName("Should return false for non-existent image")
    void testImageExistsWithNonExistentPath() {
        assertFalse(imageService.imageExists("covers/non-existent.jpg"));
    }

    @Test
    @DisplayName("Should return false for null library ID in imageExistsInLibrary")
    void testImageExistsInLibraryWithNullLibraryId() {
        assertFalse(imageService.imageExistsInLibrary("covers/test.jpg", null));
    }

    // ========================================
    // Image Deletion Tests
    // ========================================

    @Test
    @DisplayName("Should return false when deleting null path")
    void testDeleteNullPath() {
        assertFalse(imageService.deleteImage(null));
    }

    @Test
    @DisplayName("Should return false when deleting empty path")
    void testDeleteEmptyPath() {
        assertFalse(imageService.deleteImage(""));
        assertFalse(imageService.deleteImage("   "));
    }

    @Test
    @DisplayName("Should return false when deleting non-existent image")
    void testDeleteNonExistentImage() {
        assertFalse(imageService.deleteImage("covers/non-existent.jpg"));
    }

    // ========================================
    // Content Image Processing Tests
    // ========================================

    @Test
    @DisplayName("Should process content with no images")
    void testProcessContentWithNoImages() {
        // Arrange
        String htmlContent = "<p>This is plain text with no images</p>";

        // Act
        ImageService.ContentImageProcessingResult result =
            imageService.processContentImages(htmlContent, testStoryId);

        // Assert
        assertNotNull(result);
        assertEquals(htmlContent, result.getProcessedContent());
        assertTrue(result.getDownloadedImages().isEmpty());
        assertFalse(result.hasWarnings());
    }

    @Test
    @DisplayName("Should handle null content gracefully")
    void testProcessNullContent() {
        // Act
        ImageService.ContentImageProcessingResult result =
            imageService.processContentImages(null, testStoryId);

        // Assert
        assertNotNull(result);
        assertNull(result.getProcessedContent());
        assertTrue(result.getDownloadedImages().isEmpty());
    }

    @Test
    @DisplayName("Should handle empty content gracefully")
    void testProcessEmptyContent() {
        // Act
        ImageService.ContentImageProcessingResult result =
            imageService.processContentImages("", testStoryId);

        // Assert
        assertNotNull(result);
        assertEquals("", result.getProcessedContent());
        assertTrue(result.getDownloadedImages().isEmpty());
    }

    @Test
    @DisplayName("Should skip data URLs")
    void testSkipDataUrls() {
        // Arrange
        String htmlWithDataUrl = "<p><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"></p>";

        // Act
        ImageService.ContentImageProcessingResult result =
            imageService.processContentImages(htmlWithDataUrl, testStoryId);

        // Assert
        assertNotNull(result);
        assertTrue(result.getDownloadedImages().isEmpty());
        assertFalse(result.hasWarnings());
    }

    @Test
    @DisplayName("Should skip local/relative URLs")
    void testSkipLocalUrls() {
        // Arrange
        String htmlWithLocalUrl = "<p><img src=\"/images/local-image.jpg\"></p>";

        // Act
        ImageService.ContentImageProcessingResult result =
            imageService.processContentImages(htmlWithLocalUrl, testStoryId);

        // Assert
        assertNotNull(result);
        assertTrue(result.getDownloadedImages().isEmpty());
        assertFalse(result.hasWarnings());
    }

    @Test
    @DisplayName("Should skip images from same application")
    void testSkipApplicationUrls() {
        // Arrange
        String htmlWithAppUrl = "<p><img src=\"/api/files/images/default/covers/test.jpg\"></p>";

        // Act
        ImageService.ContentImageProcessingResult result =
            imageService.processContentImages(htmlWithAppUrl, testStoryId);

        // Assert
        assertNotNull(result);
        assertTrue(result.getDownloadedImages().isEmpty());
        assertFalse(result.hasWarnings());
    }

    @Test
    @DisplayName("Should handle external URL gracefully when download fails")
    void testHandleDownloadFailure() {
        // Arrange
        String htmlWithExternalUrl = "<p><img src=\"http://example.com/non-existent-image.jpg\"></p>";

        // Act
        ImageService.ContentImageProcessingResult result =
            imageService.processContentImages(htmlWithExternalUrl, testStoryId);

        // Assert
        assertNotNull(result);
        assertTrue(result.hasWarnings());
        assertEquals(1, result.getWarnings().size());
    }

    // ========================================
    // Content Image Cleanup Tests
    // ========================================

    @Test
    @DisplayName("Should perform dry run cleanup without deleting")
    void testDryRunCleanup() {
        // Arrange
        when(storyService.findAllWithAssociations()).thenReturn(new ArrayList<>());
        when(authorService.findAll()).thenReturn(new ArrayList<>());
        when(collectionService.findAllWithTags()).thenReturn(new ArrayList<>());

        // Act
        ImageService.ContentImageCleanupResult result =
            imageService.cleanupOrphanedContentImages(true);

        // Assert
        assertNotNull(result);
        assertTrue(result.isDryRun());
    }

    @Test
    @DisplayName("Should handle cleanup with no content directory")
    void testCleanupWithNoContentDirectory() {
        // Arrange
        when(storyService.findAllWithAssociations()).thenReturn(new ArrayList<>());
        when(authorService.findAll()).thenReturn(new ArrayList<>());
        when(collectionService.findAllWithTags()).thenReturn(new ArrayList<>());

        // Act
        ImageService.ContentImageCleanupResult result =
            imageService.cleanupOrphanedContentImages(false);

        // Assert
        assertNotNull(result);
        assertEquals(0, result.getTotalReferencedImages());
        assertTrue(result.getOrphanedImages().isEmpty());
    }

    @Test
    @DisplayName("Should collect image references from stories")
    void testCollectImageReferences() {
        // Arrange
        Story story = new Story();
        // Use a UUID-based filename — collectAllImageReferences() filters by isUuidBasedFilename()
        String imageFilename = UUID.randomUUID() + ".jpg";
        story.setId(testStoryId);
        story.setContentHtml("<p><img src=\"/api/files/images/default/content/" + testStoryId + "/" + imageFilename + "\"></p>");

        when(storyService.findAllWithAssociations()).thenReturn(List.of(story));
        when(authorService.findAll()).thenReturn(new ArrayList<>());
        when(collectionService.findAllWithTags()).thenReturn(new ArrayList<>());

        // Act
        ImageService.ContentImageCleanupResult result =
            imageService.cleanupOrphanedContentImages(true);

        // Assert
        assertNotNull(result);
        assertTrue(result.getTotalReferencedImages() > 0);
    }

    // ========================================
    // Cleanup Result Formatting Tests
    // ========================================

    @Test
    @DisplayName("Should format bytes correctly")
    void testFormatBytes() {
        ImageService.ContentImageCleanupResult result =
            new ImageService.ContentImageCleanupResult(
                new ArrayList<>(), 512, 0, 0, new ArrayList<>(), true
            );

        assertEquals("512 B", result.getFormattedSize());
    }

    @Test
    @DisplayName("Should format kilobytes correctly")
    void testFormatKilobytes() {
        ImageService.ContentImageCleanupResult result =
            new ImageService.ContentImageCleanupResult(
                new ArrayList<>(), 1536, 0, 0, new ArrayList<>(), true
            );

        assertTrue(result.getFormattedSize().contains("KB"));
    }

    @Test
    @DisplayName("Should format megabytes correctly")
    void testFormatMegabytes() {
        ImageService.ContentImageCleanupResult result =
            new ImageService.ContentImageCleanupResult(
                new ArrayList<>(), 1024 * 1024 * 5, 0, 0, new ArrayList<>(), true
            );

        assertTrue(result.getFormattedSize().contains("MB"));
    }

    @Test
    @DisplayName("Should format gigabytes correctly")
    void testFormatGigabytes() {
        ImageService.ContentImageCleanupResult result =
            new ImageService.ContentImageCleanupResult(
                new ArrayList<>(), 1024L * 1024L * 1024L * 2L, 0, 0, new ArrayList<>(), true
            );

        assertTrue(result.getFormattedSize().contains("GB"));
    }

    @Test
    @DisplayName("Should track cleanup errors")
    void testCleanupErrors() {
        List<String> errors = new ArrayList<>();
        errors.add("Test error 1");
        errors.add("Test error 2");

        ImageService.ContentImageCleanupResult result =
            new ImageService.ContentImageCleanupResult(
                new ArrayList<>(), 0, 0, 0, errors, false
            );

        assertTrue(result.hasErrors());
        assertEquals(2, result.getErrors().size());
    }

    // ========================================
    // Content Image Processing Result Tests
    // ========================================

    @Test
    @DisplayName("Should create processing result with warnings")
    void testProcessingResultWithWarnings() {
        List<String> warnings = List.of("Warning 1", "Warning 2");
        ImageService.ContentImageProcessingResult result =
            new ImageService.ContentImageProcessingResult(
                "<p>Content</p>", warnings, new ArrayList<>()
            );

        assertTrue(result.hasWarnings());
        assertEquals(2, result.getWarnings().size());
    }

    @Test
    @DisplayName("Should create processing result without warnings")
    void testProcessingResultWithoutWarnings() {
        ImageService.ContentImageProcessingResult result =
            new ImageService.ContentImageProcessingResult(
                "<p>Content</p>", new ArrayList<>(), new ArrayList<>()
            );

        assertFalse(result.hasWarnings());
        assertEquals("<p>Content</p>", result.getProcessedContent());
    }

    @Test
    @DisplayName("Should track downloaded images")
    void testTrackDownloadedImages() {
        List<String> downloadedImages = List.of(
            "content/story1/image1.jpg",
            "content/story1/image2.jpg"
        );

        ImageService.ContentImageProcessingResult result =
            new ImageService.ContentImageProcessingResult(
                "<p>Content</p>", new ArrayList<>(), downloadedImages
            );

        assertEquals(2, result.getDownloadedImages().size());
        assertTrue(result.getDownloadedImages().contains("content/story1/image1.jpg"));
    }

    // ========================================
    // Story Content Deletion Tests
    // ========================================

    @Test
    @DisplayName("Should delete content images for story")
    void testDeleteContentImages() {
        // Act - Should not throw exception even if directory doesn't exist
        assertDoesNotThrow(() -> {
            imageService.deleteContentImages(testStoryId);
        });
    }

    // ========================================
    // Edge Cases
    // ========================================

    @Test
    @DisplayName("Should handle HTML with multiple images")
    void testMultipleImages() {
        // Arrange
        String html = "<p><img src=\"/local1.jpg\"><img src=\"/local2.jpg\"></p>";

        // Act
        ImageService.ContentImageProcessingResult result =
            imageService.processContentImages(html, testStoryId);

        // Assert
        assertNotNull(result);
        // Local images should be skipped
        assertTrue(result.getDownloadedImages().isEmpty());
    }

    @Test
    @DisplayName("Should handle malformed HTML gracefully")
    void testMalformedHtml() {
        // Arrange
        String malformedHtml = "<p>Unclosed <img src=\"/test.jpg\" <p>";

        // Act
        ImageService.ContentImageProcessingResult result =
            imageService.processContentImages(malformedHtml, testStoryId);

        // Assert
        assertNotNull(result);
    }

    @Test
    @DisplayName("Should handle very long content")
    void testVeryLongContent() {
        // Arrange
        StringBuilder longContent = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            longContent.append("<p>Paragraph ").append(i).append("</p>");
        }

        // Act
        ImageService.ContentImageProcessingResult result =
            imageService.processContentImages(longContent.toString(), testStoryId);

        // Assert
        assertNotNull(result);
    }

    // ========================================
    // uploadContentImage Tests
    // ========================================

    @Test
    @DisplayName("uploadContentImage should save file into story UUID subfolder")
    void uploadContentImage_savesToStorySubfolder() throws IOException {
        // Arrange
        Path contentDir = tempDir.resolve("default").resolve("content");

        // Act
        String resultPath = imageService.uploadContentImage(validImageFile, testStoryId);

        // Assert
        assertTrue(resultPath.startsWith("content/" + testStoryId + "/"),
            "Path should be content/{storyId}/filename but was: " + resultPath);
        Path savedFile = tempDir.resolve("default").resolve(resultPath);
        assertTrue(Files.exists(savedFile), "Saved file should exist on disk");
        assertTrue(Files.exists(contentDir.resolve(testStoryId.toString())),
            "Story UUID subdirectory should have been created");
    }

    @Test
    @DisplayName("uploadContentImage should reject invalid file")
    void uploadContentImage_rejectsInvalidFile() {
        MockMultipartFile emptyFile = new MockMultipartFile("image", "test.png", "image/png", new byte[0]);
        assertThrows(IllegalArgumentException.class,
            () -> imageService.uploadContentImage(emptyFile, testStoryId));
    }

    // ========================================
    // migrateEpubContentImages Tests
    // ========================================

    @Test
    @DisplayName("migrateEpubContentImages dry run should identify flat files with story matches")
    void migrateEpubContentImages_dryRun_identifiesCorrectFiles() throws IOException {
        // Arrange: place a flat image in content/
        Path contentDir = tempDir.resolve("default").resolve("content");
        Files.createDirectories(contentDir);
        String filename = "epub-img-12345.png";
        Files.write(contentDir.resolve(filename), createMinimalPngData());

        String libraryId = "default";
        Story story = new Story();
        story.setId(testStoryId);
        story.setTitle("Test Story");
        story.setContentHtml("<img src=\"/api/files/images/" + libraryId + "/content/" + filename + "\">");

        when(storyService.findAllWithAssociations()).thenReturn(List.of(story));
        when(libraryService.getCurrentLibraryId()).thenReturn(libraryId);

        // Act
        ImageService.EpubImageMigrationResult result = imageService.migrateEpubContentImages(true);

        // Assert
        assertEquals(1, result.getMovedCount());
        assertEquals(0, result.getUnmatchedCount());
        assertTrue(result.isDryRun());
        assertFalse(result.hasErrors());
        // Dry run: file should still be at original location
        assertTrue(Files.exists(contentDir.resolve(filename)), "Dry run must not move file");
    }

    @Test
    @DisplayName("migrateEpubContentImages execute should move file and story content is updated")
    void migrateEpubContentImages_execute_movesFilesAndUpdatesContent() throws IOException {
        // Arrange
        Path contentDir = tempDir.resolve("default").resolve("content");
        Files.createDirectories(contentDir);
        String filename = "epub-img-99999.png";
        Files.write(contentDir.resolve(filename), createMinimalPngData());

        String libraryId = "default";
        String oldUrl = "/api/files/images/" + libraryId + "/content/" + filename;
        Story story = new Story();
        story.setId(testStoryId);
        story.setTitle("Test Story");
        story.setContentHtml("<img src=\"" + oldUrl + "\">");

        when(storyService.findAllWithAssociations()).thenReturn(List.of(story));
        when(libraryService.getCurrentLibraryId()).thenReturn(libraryId);
        when(storyService.update(eq(testStoryId), any())).thenReturn(story);

        // Act
        ImageService.EpubImageMigrationResult result = imageService.migrateEpubContentImages(false);

        // Assert
        assertEquals(1, result.getMovedCount());
        assertFalse(result.isDryRun());
        assertFalse(result.hasErrors());
        // Original flat file should be gone
        assertFalse(Files.exists(contentDir.resolve(filename)), "Flat file should have been moved");
        // File should now exist in story subfolder
        assertTrue(Files.exists(contentDir.resolve(testStoryId.toString()).resolve(filename)),
            "File should now be in story subfolder");
        // storyService.update should have been called to update content URLs
        verify(storyService).update(eq(testStoryId), any());
    }

    @Test
    @DisplayName("migrateEpubContentImages should leave unmatched files in place")
    void migrateEpubContentImages_unmatchedFile_notMoved() throws IOException {
        // Arrange: flat image with no story referencing it
        Path contentDir = tempDir.resolve("default").resolve("content");
        Files.createDirectories(contentDir);
        String filename = "orphaned-img.png";
        Files.write(contentDir.resolve(filename), createMinimalPngData());

        Story story = new Story();
        story.setId(testStoryId);
        story.setTitle("Unrelated Story");
        story.setContentHtml("<p>No images here</p>");

        when(storyService.findAllWithAssociations()).thenReturn(List.of(story));

        // Act
        ImageService.EpubImageMigrationResult result = imageService.migrateEpubContentImages(false);

        // Assert
        assertEquals(0, result.getMovedCount());
        assertEquals(1, result.getUnmatchedCount());
        assertTrue(Files.exists(contentDir.resolve(filename)), "Unmatched file must remain");
        verify(storyService, never()).update(any(), any());
    }

    // ========================================
    // Helper Methods
    // ========================================

    /**
     * Create minimal valid WebP data for testing.
     * This is the smallest possible WebP file (a 1x1 pixel lossless image).
     */
    private byte[] createMinimalWebpData() {
        // Minimal WebP file: RIFF header + WEBP marker + VP8L chunk for a 1x1 white pixel
        return new byte[]{
            'R', 'I', 'F', 'F',                          // RIFF marker
            0x24, 0x00, 0x00, 0x00,                       // File size minus 8 (36 bytes)
            'W', 'E', 'B', 'P',                           // WEBP marker
            'V', 'P', '8', 'L',                           // VP8L chunk marker (lossless)
            0x14, 0x00, 0x00, 0x00,                       // Chunk size (20 bytes)
            0x2F,                                          // Signature byte
            0x00, 0x00, 0x00, 0x00,                       // Width-1=0, Height-1=0, alpha, version
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,    // Image data (white pixel)
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
        };
    }

    /**
     * Create minimal valid PNG data for testing.
     * This is a 1x1 pixel transparent PNG image.
     */
    private byte[] createMinimalPngData() {
        return new byte[]{
            (byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n', // PNG signature
            0x00, 0x00, 0x00, 0x0D, // IHDR chunk length
            'I', 'H', 'D', 'R', // IHDR chunk type
            0x00, 0x00, 0x00, 0x01, // Width: 1
            0x00, 0x00, 0x00, 0x01, // Height: 1
            0x08, // Bit depth: 8
            0x06, // Color type: RGBA
            0x00, 0x00, 0x00, // Compression, filter, interlace
            0x1F, 0x15, (byte) 0xC4, (byte) 0x89, // CRC
            0x00, 0x00, 0x00, 0x0A, // IDAT chunk length
            'I', 'D', 'A', 'T', // IDAT chunk type
            0x78, (byte) 0x9C, 0x62, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, // Image data
            0x0D, 0x0A, 0x2D, (byte) 0xB4, // CRC
            0x00, 0x00, 0x00, 0x00, // IEND chunk length
            'I', 'E', 'N', 'D', // IEND chunk type
            (byte) 0xAE, 0x42, 0x60, (byte) 0x82 // CRC
        };
    }
}
