"""Unit tests for VIN preprocessor.""" import io from unittest.mock import patch, MagicMock import numpy as np import pytest from PIL import Image from app.preprocessors.vin_preprocessor import VinPreprocessor, vin_preprocessor def create_test_image(width: int = 400, height: int = 100, color: int = 128) -> bytes: """Create a simple test image.""" image = Image.new("RGB", (width, height), (color, color, color)) buffer = io.BytesIO() image.save(buffer, format="PNG") return buffer.getvalue() def create_grayscale_test_image(width: int = 400, height: int = 100) -> bytes: """Create a grayscale test image.""" image = Image.new("L", (width, height), 128) buffer = io.BytesIO() image.save(buffer, format="PNG") return buffer.getvalue() class TestVinPreprocessor: """Tests for VIN-optimized preprocessing.""" def test_preprocess_returns_result(self) -> None: """Test basic preprocessing returns a result.""" preprocessor = VinPreprocessor() image_bytes = create_test_image() result = preprocessor.preprocess(image_bytes) assert result.image_bytes is not None assert len(result.image_bytes) > 0 assert "grayscale" in result.preprocessing_applied def test_preprocess_applies_all_steps(self) -> None: """Test preprocessing applies all requested steps.""" preprocessor = VinPreprocessor() image_bytes = create_test_image() result = preprocessor.preprocess( image_bytes, apply_clahe=True, apply_deskew=True, apply_denoise=True, apply_threshold=True, ) assert "grayscale" in result.preprocessing_applied assert "clahe" in result.preprocessing_applied assert "deskew" in result.preprocessing_applied assert "denoise" in result.preprocessing_applied assert "threshold" in result.preprocessing_applied def test_preprocess_skips_disabled_steps(self) -> None: """Test preprocessing skips disabled steps.""" preprocessor = VinPreprocessor() image_bytes = create_test_image() result = preprocessor.preprocess( image_bytes, apply_clahe=False, apply_deskew=False, apply_denoise=False, apply_threshold=False, ) assert "clahe" not in result.preprocessing_applied assert "deskew" not in result.preprocessing_applied assert "denoise" not in result.preprocessing_applied assert "threshold" not in result.preprocessing_applied def test_preprocess_output_is_valid_image(self) -> None: """Test preprocessing output is a valid PNG image.""" preprocessor = VinPreprocessor() image_bytes = create_test_image() result = preprocessor.preprocess(image_bytes) # Should be able to open as image output_image = Image.open(io.BytesIO(result.image_bytes)) assert output_image is not None assert output_image.format == "PNG" def test_preprocess_handles_grayscale_input(self) -> None: """Test preprocessing handles grayscale input.""" preprocessor = VinPreprocessor() image_bytes = create_grayscale_test_image() result = preprocessor.preprocess(image_bytes) assert result.image_bytes is not None assert len(result.image_bytes) > 0 def test_preprocess_handles_rgba_input(self) -> None: """Test preprocessing handles RGBA input.""" preprocessor = VinPreprocessor() # Create RGBA image image = Image.new("RGBA", (400, 100), (128, 128, 128, 255)) buffer = io.BytesIO() image.save(buffer, format="PNG") result = preprocessor.preprocess(buffer.getvalue()) assert result.image_bytes is not None assert "convert_rgb" in result.preprocessing_applied def test_singleton_instance(self) -> None: """Test singleton instance is available.""" assert vin_preprocessor is not None assert isinstance(vin_preprocessor, VinPreprocessor) class TestVinPreprocessorDeskew: """Tests for deskew functionality.""" def test_deskew_no_change_for_straight_image(self) -> None: """Test deskew doesn't change a straight image significantly.""" preprocessor = VinPreprocessor() # Create image with horizontal line (no skew) image = np.zeros((100, 400), dtype=np.uint8) image[50, 50:350] = 255 # Horizontal line result = preprocessor._deskew(image) # Shape should be similar (might change slightly due to processing) assert result.shape[0] > 0 assert result.shape[1] > 0 class TestVinPreprocessorCLAHE: """Tests for CLAHE contrast enhancement.""" def test_clahe_improves_contrast(self) -> None: """Test CLAHE changes the image.""" preprocessor = VinPreprocessor() # Create low contrast image image = np.full((100, 400), 128, dtype=np.uint8) result = preprocessor._apply_clahe(image) # Result should be numpy array of same shape assert result.shape == image.shape class TestVinPreprocessorDenoise: """Tests for denoising functionality.""" def test_denoise_reduces_noise(self) -> None: """Test denoising works on noisy image.""" preprocessor = VinPreprocessor() # Create noisy image image = np.random.randint(0, 256, (100, 400), dtype=np.uint8) result = preprocessor._denoise(image) # Should return array of same shape assert result.shape == image.shape class TestVinPreprocessorThreshold: """Tests for adaptive thresholding.""" def test_threshold_creates_binary_image(self) -> None: """Test thresholding creates binary output.""" preprocessor = VinPreprocessor() # Create grayscale image image = np.full((100, 400), 128, dtype=np.uint8) result = preprocessor._adaptive_threshold(image) # Result should be binary (only 0 and 255) unique_values = np.unique(result) assert len(unique_values) <= 2 class TestVinRegionDetection: """Tests for VIN region detection.""" def test_detect_vin_region_returns_none_for_empty(self) -> None: """Test region detection returns None for empty image.""" preprocessor = VinPreprocessor() # Solid color image - no regions to detect image_bytes = create_test_image(color=128) result = preprocessor.detect_vin_region(image_bytes) # May return None for uniform image # This is expected behavior assert result is None or result.width > 0