"""Unit tests for pipeline steps.

This module provides comprehensive tests for all pipeline step types:
- PoolStep: Spatial downsampling with various methods and color spaces
- QuantizeStep: Color matching to palette without error diffusion
- DitherStep: Color matching with error diffusion dithering

Tests cover:
- All aggregation methods (mean, median, mode, max, min)
- All color spaces (RGB, Lab, Linear RGB)
- All dithering algorithms (Floyd-Steinberg, Atkinson, etc.)
- Edge cases (uniform images, single pixels, non-uniform strides)
- Input/output type validation
- Configuration validation
"""

import numpy as np
import pytest

from legopic import Color, Palette
from legopic.pipeline import (
    ColorSpace,
    DitherAlgorithm,
    DitherConfig,
    DitherStep,
    IndexMap,
    PipelineContext,
    PoolConfig,
    PoolMethod,
    PoolStep,
    QuantizeConfig,
    QuantizeStep,
    RGBImage,
    ScanOrder,
)


class TestPoolStep:
    """Tests for PoolStep spatial downsampling."""

    @pytest.fixture
    def simple_palette(self):
        """A simple 3-color palette."""
        return Palette(
            [
                Color((255, 0, 0), name="Red"),
                Color((0, 255, 0), name="Green"),
                Color((0, 0, 255), name="Blue"),
            ]
        )

    def test_pool_rgb_mean(self, simple_palette):
        """PoolStep with MEAN method averages pixel values."""
        # Create a 10x10 image with gradient from black to white
        data = np.zeros((10, 10, 3), dtype=np.uint8)
        data[:, :5, :] = 0  # Left half black
        data[:, 5:, :] = 100  # Right half gray

        image = RGBImage(data=data)
        step = PoolStep(PoolConfig(method=PoolMethod.MEAN))
        context = PipelineContext(palette=simple_palette, target_size=(2, 2))

        result = step.process(image, context)

        assert isinstance(result, RGBImage)
        assert result.shape == (2, 2)

    def test_pool_rgb_mean_value_correctness(self, simple_palette):
        """MEAN method computes correct average values."""
        # Create 2x2 image with known values
        data = np.array(
            [
                [[0, 0, 0], [100, 100, 100]],
                [[100, 100, 100], [200, 200, 200]],
            ],
            dtype=np.uint8,
        )
        image = RGBImage(data=data)
        step = PoolStep(PoolConfig(method=PoolMethod.MEAN))
        context = PipelineContext(palette=simple_palette, target_size=(1, 1))

        result = step.process(image, context)

        # Mean of [0, 100, 100, 200] = 100
        np.testing.assert_array_equal(result.data[0, 0], [100, 100, 100])

    def test_pool_rgb_median(self, simple_palette):
        """PoolStep with MEDIAN method computes median values."""
        # Create 4x4 image with outlier
        data = np.full((4, 4, 3), 100, dtype=np.uint8)
        data[0, 0] = [255, 255, 255]  # Outlier

        image = RGBImage(data=data)
        step = PoolStep(PoolConfig(method=PoolMethod.MEDIAN))
        context = PipelineContext(palette=simple_palette, target_size=(1, 1))

        result = step.process(image, context)

        # Median should ignore outlier, result close to 100
        assert result.data[0, 0, 0] == 100

    def test_pool_rgb_max(self, simple_palette):
        """PoolStep with MAX method finds maximum values."""
        data = np.array(
            [
                [[50, 50, 50], [100, 100, 100]],
                [[150, 150, 150], [200, 200, 200]],
            ],
            dtype=np.uint8,
        )
        image = RGBImage(data=data)
        step = PoolStep(PoolConfig(method=PoolMethod.MAX))
        context = PipelineContext(palette=simple_palette, target_size=(1, 1))

        result = step.process(image, context)

        np.testing.assert_array_equal(result.data[0, 0], [200, 200, 200])

    def test_pool_rgb_min(self, simple_palette):
        """PoolStep with MIN method finds minimum values."""
        data = np.array(
            [
                [[50, 50, 50], [100, 100, 100]],
                [[150, 150, 150], [200, 200, 200]],
            ],
            dtype=np.uint8,
        )
        image = RGBImage(data=data)
        step = PoolStep(PoolConfig(method=PoolMethod.MIN))
        context = PipelineContext(palette=simple_palette, target_size=(1, 1))

        result = step.process(image, context)

        np.testing.assert_array_equal(result.data[0, 0], [50, 50, 50])

    def test_pool_rgb_mode(self, simple_palette):
        """PoolStep with MODE method finds most common color."""
        # Create 4x4 image with mostly red, one blue
        data = np.full((4, 4, 3), [255, 0, 0], dtype=np.uint8)
        data[0, 0] = [0, 0, 255]  # One blue pixel

        image = RGBImage(data=data)
        step = PoolStep(PoolConfig(method=PoolMethod.MODE))
        context = PipelineContext(palette=simple_palette, target_size=(1, 1))

        result = step.process(image, context)

        # Mode should be red (most common)
        np.testing.assert_array_equal(result.data[0, 0], [255, 0, 0])

    def test_pool_rgb_lab_colorspace(self, simple_palette):
        """PoolStep with LAB color space runs without error."""
        data = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        step = PoolStep(PoolConfig(method=PoolMethod.MEAN, color_space=ColorSpace.LAB))
        context = PipelineContext(palette=simple_palette, target_size=(10, 10))

        result = step.process(image, context)

        assert isinstance(result, RGBImage)
        assert result.shape == (10, 10)

    def test_pool_rgb_linear_colorspace(self, simple_palette):
        """PoolStep with LINEAR_RGB color space runs without error."""
        data = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        step = PoolStep(PoolConfig(method=PoolMethod.MEAN, color_space=ColorSpace.LINEAR_RGB))
        context = PipelineContext(palette=simple_palette, target_size=(10, 10))

        result = step.process(image, context)

        assert isinstance(result, RGBImage)
        assert result.shape == (10, 10)

    @pytest.mark.parametrize("color_space", [ColorSpace.RGB, ColorSpace.LAB, ColorSpace.LINEAR_RGB])
    def test_pool_all_colorspaces_uniform_image(self, color_space, simple_palette):
        """All color spaces preserve uniform color images."""
        data = np.full((10, 10, 3), [128, 64, 192], dtype=np.uint8)
        image = RGBImage(data=data)

        step = PoolStep(PoolConfig(method=PoolMethod.MEAN, color_space=color_space))
        context = PipelineContext(palette=simple_palette, target_size=(5, 5))

        result = step.process(image, context)

        # Uniform input should produce uniform output (within tolerance)
        # Check all pixels have the expected color
        expected = np.full((5, 5, 3), [128, 64, 192], dtype=np.uint8)
        np.testing.assert_allclose(result.data, expected, atol=2)

    def test_pool_index_uses_mode(self, simple_palette):
        """PoolStep on IndexMap always uses mode."""
        # Create index map with mostly 0s and some 1s
        data = np.zeros((10, 10), dtype=np.intp)
        data[0, 0] = 1  # One different value

        index_map = IndexMap(data=data, palette=simple_palette)
        step = PoolStep(PoolConfig())
        context = PipelineContext(palette=simple_palette, target_size=(1, 1))

        result = step.process(index_map, context)

        assert isinstance(result, IndexMap)
        assert result.shape == (1, 1)
        # Mode should be 0 (most common)
        assert result.data[0, 0] == 0

    def test_pool_index_preserves_palette(self, simple_palette):
        """IndexMap pooling preserves palette reference."""
        data = np.zeros((10, 10), dtype=np.intp)
        index_map = IndexMap(data=data, palette=simple_palette)

        step = PoolStep(PoolConfig())
        context = PipelineContext(palette=simple_palette, target_size=(5, 5))

        result = step.process(index_map, context)

        assert result.palette is simple_palette

    def test_pool_upsampling_raises(self, simple_palette):
        """PoolStep rejects upsampling."""
        data = np.zeros((10, 10, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        step = PoolStep(PoolConfig(output_size=(20, 20)))  # Larger than input
        context = PipelineContext(palette=simple_palette, target_size=(20, 20))

        with pytest.raises(ValueError, match="Cannot upsample"):
            step.process(image, context)

    def test_pool_non_uniform_stride_raises(self, simple_palette):
        """PoolStep rejects non-uniform stride (width ratio != height ratio)."""
        # 100x50 image pooled to 10x10 would need stride 10x5 (non-uniform)
        data = np.zeros((50, 100, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        step = PoolStep(PoolConfig(output_size=(10, 10)))
        context = PipelineContext(palette=simple_palette, target_size=(10, 10))

        with pytest.raises(ValueError, match="Non-uniform stride"):
            step.process(image, context)

    def test_pool_uses_config_output_size(self, simple_palette):
        """PoolStep uses config output_size over context target_size."""
        data = np.zeros((100, 100, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        # Config says 20x20, context says 10x10
        step = PoolStep(PoolConfig(output_size=(20, 20)))
        context = PipelineContext(palette=simple_palette, target_size=(10, 10))

        result = step.process(image, context)

        # Should use config's output_size
        assert result.shape == (20, 20)

    def test_pool_uses_context_target_when_no_config(self, simple_palette):
        """PoolStep uses context target_size when config output_size is None."""
        data = np.zeros((100, 100, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        step = PoolStep(PoolConfig(output_size=None))
        context = PipelineContext(palette=simple_palette, target_size=(25, 25))

        result = step.process(image, context)

        assert result.shape == (25, 25)

    def test_pool_input_types_property(self):
        """PoolStep.input_types returns correct types."""
        step = PoolStep()

        assert RGBImage in step.input_types
        assert IndexMap in step.input_types

    def test_pool_output_type_for_rgb(self):
        """PoolStep.output_type_for_input returns RGBImage for RGBImage input."""
        step = PoolStep()

        assert step.output_type_for_input(RGBImage) == RGBImage

    def test_pool_output_type_for_index(self):
        """PoolStep.output_type_for_input returns IndexMap for IndexMap input."""
        step = PoolStep()

        assert step.output_type_for_input(IndexMap) == IndexMap

    def test_pool_output_type_invalid_raises(self):
        """PoolStep.output_type_for_input raises for invalid type."""
        step = PoolStep()

        with pytest.raises(TypeError):
            step.output_type_for_input(str)

    def test_pool_repr(self):
        """PoolStep repr includes config info."""
        config = PoolConfig(output_size=(48, 48), method=PoolMethod.MEDIAN)
        step = PoolStep(config)

        repr_str = repr(step)

        assert "PoolStep" in repr_str
        assert "config=" in repr_str


class TestQuantizeStep:
    """Tests for QuantizeStep color quantization."""

    @pytest.fixture
    def simple_palette(self):
        """A simple 3-color palette."""
        return Palette(
            [
                Color((255, 0, 0), name="Red"),
                Color((0, 255, 0), name="Green"),
                Color((0, 0, 255), name="Blue"),
            ]
        )

    @pytest.fixture
    def grayscale_palette(self):
        """A grayscale palette for testing neutral colors."""
        return Palette(
            [
                Color((0, 0, 0), name="Black"),
                Color((128, 128, 128), name="Gray"),
                Color((255, 255, 255), name="White"),
            ]
        )

    def test_quantize_exact_match(self, simple_palette):
        """QuantizeStep finds exact palette matches."""
        # Create image with exact palette colors
        data = np.zeros((2, 3, 3), dtype=np.uint8)
        data[0, 0] = [255, 0, 0]  # Red
        data[0, 1] = [0, 255, 0]  # Green
        data[0, 2] = [0, 0, 255]  # Blue
        data[1, :] = data[0, :]

        image = RGBImage(data=data)
        step = QuantizeStep()
        context = PipelineContext(palette=simple_palette, target_size=(3, 2))

        result = step.process(image, context)

        assert isinstance(result, IndexMap)
        assert result.shape == (2, 3)

        # Verify indices
        assert result.data[0, 0] == 0  # Red
        assert result.data[0, 1] == 1  # Green
        assert result.data[0, 2] == 2  # Blue

    def test_quantize_nearest_match(self, simple_palette):
        """QuantizeStep finds nearest palette color."""
        # Create image with slightly off colors
        data = np.zeros((1, 1, 3), dtype=np.uint8)
        data[0, 0] = [250, 10, 10]  # Almost red

        image = RGBImage(data=data)
        step = QuantizeStep()
        context = PipelineContext(palette=simple_palette, target_size=(1, 1))

        result = step.process(image, context)

        # Should match to red (index 0)
        assert result.data[0, 0] == 0

    def test_quantize_grayscale_nearest(self, grayscale_palette):
        """QuantizeStep finds nearest grayscale color."""
        data = np.array(
            [
                [[10, 10, 10]],  # Near black
                [[120, 120, 120]],  # Near gray
                [[240, 240, 240]],  # Near white
            ],
            dtype=np.uint8,
        )
        image = RGBImage(data=data)
        step = QuantizeStep()
        context = PipelineContext(palette=grayscale_palette, target_size=(1, 3))

        result = step.process(image, context)

        assert result.data[0, 0] == 0  # Black
        assert result.data[1, 0] == 1  # Gray
        assert result.data[2, 0] == 2  # White

    def test_quantize_preserves_palette(self, simple_palette):
        """QuantizeStep result references context palette."""
        data = np.zeros((5, 5, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        step = QuantizeStep()
        context = PipelineContext(palette=simple_palette, target_size=(5, 5))

        result = step.process(image, context)

        assert result.palette is simple_palette

    def test_quantize_uniform_image(self, simple_palette):
        """Uniform color image produces uniform index map."""
        data = np.full((10, 10, 3), [255, 0, 0], dtype=np.uint8)
        image = RGBImage(data=data)

        step = QuantizeStep()
        context = PipelineContext(palette=simple_palette, target_size=(10, 10))

        result = step.process(image, context)

        # All indices should be 0 (red)
        assert np.all(result.data == 0)

    def test_quantize_input_types_property(self):
        """QuantizeStep.input_types returns correct types."""
        step = QuantizeStep()

        assert step.input_types == (RGBImage,)

    def test_quantize_output_type_for_rgb(self):
        """QuantizeStep.output_type_for_input returns IndexMap for RGBImage."""
        step = QuantizeStep()

        assert step.output_type_for_input(RGBImage) == IndexMap

    def test_quantize_output_type_invalid_raises(self):
        """QuantizeStep.output_type_for_input raises for invalid type."""
        step = QuantizeStep()

        with pytest.raises(TypeError):
            step.output_type_for_input(IndexMap)

    def test_quantize_rejects_indexmap_input(self, simple_palette):
        """QuantizeStep.process raises TypeError for IndexMap input."""
        data = np.zeros((5, 5), dtype=np.intp)
        index_map = IndexMap(data=data, palette=simple_palette)

        step = QuantizeStep()
        context = PipelineContext(palette=simple_palette, target_size=(5, 5))

        with pytest.raises(TypeError, match="Expected RGBImage"):
            step.process(index_map, context)

    def test_quantize_repr(self):
        """QuantizeStep repr includes config info."""
        config = QuantizeConfig(metric="delta_e")
        step = QuantizeStep(config)

        repr_str = repr(step)

        assert "QuantizeStep" in repr_str
        assert "config=" in repr_str

    def test_quantize_default_config(self):
        """QuantizeStep uses default config when None provided."""
        step = QuantizeStep()

        assert step.config is not None
        assert step.config.metric == "delta_e"


class TestDitherStep:
    """Tests for DitherStep error diffusion dithering."""

    @pytest.fixture
    def simple_palette(self):
        """A simple 5-color palette."""
        return Palette(
            [
                Color((255, 0, 0), name="Red"),
                Color((0, 255, 0), name="Green"),
                Color((0, 0, 255), name="Blue"),
                Color((0, 0, 0), name="Black"),
                Color((255, 255, 255), name="White"),
            ]
        )

    @pytest.fixture
    def grayscale_palette(self):
        """A grayscale palette for gradient testing."""
        return Palette(
            [
                Color((0, 0, 0), name="Black"),
                Color((85, 85, 85), name="Dark Gray"),
                Color((170, 170, 170), name="Light Gray"),
                Color((255, 255, 255), name="White"),
            ]
        )

    @pytest.mark.parametrize(
        "algorithm",
        [
            DitherAlgorithm.FLOYD_STEINBERG,
            DitherAlgorithm.ATKINSON,
            DitherAlgorithm.JARVIS_JUDICE_NINKE,
            DitherAlgorithm.STUCKI,
            DitherAlgorithm.SIERRA,
            DitherAlgorithm.SIERRA_LITE,
            DitherAlgorithm.BAYER,
        ],
    )
    def test_all_algorithms_run(self, algorithm, simple_palette):
        """All dithering algorithms execute without error."""
        data = np.random.randint(0, 256, (20, 20, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        step = DitherStep(DitherConfig(algorithm=algorithm))
        context = PipelineContext(palette=simple_palette, target_size=(20, 20))

        result = step.process(image, context)

        assert isinstance(result, IndexMap)
        assert result.shape == (20, 20)

    @pytest.mark.parametrize(
        "algorithm",
        [
            DitherAlgorithm.FLOYD_STEINBERG,
            DitherAlgorithm.ATKINSON,
            DitherAlgorithm.JARVIS_JUDICE_NINKE,
            DitherAlgorithm.STUCKI,
            DitherAlgorithm.SIERRA,
            DitherAlgorithm.SIERRA_LITE,
        ],
    )
    def test_error_diffusion_algorithms_on_gradient(self, algorithm, grayscale_palette):
        """Error diffusion algorithms produce varied output on gradients."""
        # Create a gradient image
        gradient = np.zeros((20, 20, 3), dtype=np.uint8)
        for x in range(20):
            gradient[:, x, :] = int(x * 255 / 19)

        image = RGBImage(data=gradient)
        step = DitherStep(DitherConfig(algorithm=algorithm))
        context = PipelineContext(palette=grayscale_palette, target_size=(20, 20))

        result = step.process(image, context)

        # Dithered gradient should use multiple palette colors
        unique_indices = np.unique(result.data)
        assert len(unique_indices) >= 2

    @pytest.mark.parametrize("order", [ScanOrder.RASTER, ScanOrder.SERPENTINE])
    def test_scan_orders(self, order, simple_palette):
        """Both scan orders execute without error."""
        data = np.random.randint(0, 256, (20, 20, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        step = DitherStep(DitherConfig(order=order))
        context = PipelineContext(palette=simple_palette, target_size=(20, 20))

        result = step.process(image, context)

        assert isinstance(result, IndexMap)

    def test_serpentine_vs_raster_different_results(self, simple_palette):
        """Serpentine and raster scan orders produce different results on gradients."""
        np.random.seed(42)
        # Create image with gradient
        gradient = np.zeros((10, 10, 3), dtype=np.uint8)
        for y in range(10):
            gradient[y, :, :] = int(y * 255 / 9)

        image = RGBImage(data=gradient)
        context = PipelineContext(palette=simple_palette, target_size=(10, 10))

        raster_step = DitherStep(DitherConfig(order=ScanOrder.RASTER))
        serpentine_step = DitherStep(DitherConfig(order=ScanOrder.SERPENTINE))

        raster_result = raster_step.process(image, context)
        serpentine_result = serpentine_step.process(image, context)

        # Results may differ due to different scan patterns
        # (this is expected behavior, not a bug)
        # Just verify both run successfully
        assert raster_result.shape == (10, 10)
        assert serpentine_result.shape == (10, 10)

    def test_zero_strength_equals_quantize(self, simple_palette):
        """Dither with strength=0.0 should produce same result as QuantizeStep."""
        np.random.seed(42)
        data = np.random.randint(0, 256, (10, 10, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        dither_step = DitherStep(DitherConfig(strength=0.0))
        quantize_step = QuantizeStep()
        context = PipelineContext(palette=simple_palette, target_size=(10, 10))

        dither_result = dither_step.process(image, context)
        quantize_result = quantize_step.process(image, context)

        # With strength=0, no error diffusion occurs
        # Results should be identical
        np.testing.assert_array_equal(dither_result.data, quantize_result.data)

    def test_strength_affects_result(self, simple_palette):
        """Different strength values produce different results."""
        np.random.seed(42)
        data = np.random.randint(0, 256, (20, 20, 3), dtype=np.uint8)
        image = RGBImage(data=data)
        context = PipelineContext(palette=simple_palette, target_size=(20, 20))

        full_strength = DitherStep(DitherConfig(strength=1.0))
        half_strength = DitherStep(DitherConfig(strength=0.5))

        full_result = full_strength.process(image, context)
        half_result = half_strength.process(image, context)

        # Results should differ (error diffusion is scaled differently)
        # Not guaranteed to be different for all images, but likely for random
        # Just verify both run successfully
        assert full_result.shape == half_result.shape

    def test_uniform_image_uniform_result(self, simple_palette):
        """Uniform color image produces uniform dithered result."""
        # Uniform red image
        data = np.full((10, 10, 3), [255, 0, 0], dtype=np.uint8)
        image = RGBImage(data=data)

        step = DitherStep()
        context = PipelineContext(palette=simple_palette, target_size=(10, 10))

        result = step.process(image, context)

        # All pixels should be red (index 0)
        assert np.all(result.data == 0)

    def test_bayer_dithering_on_gradient(self, grayscale_palette):
        """Bayer ordered dithering produces pattern on gradients."""
        # Create horizontal gradient
        gradient = np.zeros((16, 16, 3), dtype=np.uint8)
        for x in range(16):
            gradient[:, x, :] = int(x * 255 / 15)

        image = RGBImage(data=gradient)
        step = DitherStep(DitherConfig(algorithm=DitherAlgorithm.BAYER))
        context = PipelineContext(palette=grayscale_palette, target_size=(16, 16))

        result = step.process(image, context)

        # Bayer dithering should produce varied output
        unique_indices = np.unique(result.data)
        assert len(unique_indices) >= 2

    def test_bayer_on_uniform_mid_gray(self, grayscale_palette):
        """Bayer dithering on mid-gray creates dither pattern."""
        # Mid-gray (128) is between palette colors
        data = np.full((8, 8, 3), 128, dtype=np.uint8)
        image = RGBImage(data=data)

        step = DitherStep(DitherConfig(algorithm=DitherAlgorithm.BAYER))
        context = PipelineContext(palette=grayscale_palette, target_size=(8, 8))

        result = step.process(image, context)

        # Bayer on mid-gray should produce a mix of dark and light gray
        unique_indices = np.unique(result.data)
        assert len(unique_indices) >= 2

    def test_invalid_strength_raises(self):
        """DitherConfig rejects invalid strength values."""
        with pytest.raises(ValueError, match="strength must be"):
            DitherConfig(strength=1.5)

        with pytest.raises(ValueError, match="strength must be"):
            DitherConfig(strength=-0.1)

    def test_dither_preserves_palette(self, simple_palette):
        """DitherStep result references context palette."""
        data = np.zeros((5, 5, 3), dtype=np.uint8)
        image = RGBImage(data=data)

        step = DitherStep()
        context = PipelineContext(palette=simple_palette, target_size=(5, 5))

        result = step.process(image, context)

        assert result.palette is simple_palette

    def test_dither_input_types_property(self):
        """DitherStep.input_types returns correct types."""
        step = DitherStep()

        assert step.input_types == (RGBImage,)

    def test_dither_output_type_for_rgb(self):
        """DitherStep.output_type_for_input returns IndexMap for RGBImage."""
        step = DitherStep()

        assert step.output_type_for_input(RGBImage) == IndexMap

    def test_dither_output_type_invalid_raises(self):
        """DitherStep.output_type_for_input raises for invalid type."""
        step = DitherStep()

        with pytest.raises(TypeError):
            step.output_type_for_input(IndexMap)

    def test_dither_rejects_indexmap_input(self, simple_palette):
        """DitherStep.process raises TypeError for IndexMap input."""
        data = np.zeros((5, 5), dtype=np.intp)
        index_map = IndexMap(data=data, palette=simple_palette)

        step = DitherStep()
        context = PipelineContext(palette=simple_palette, target_size=(5, 5))

        with pytest.raises(TypeError, match="Expected RGBImage"):
            step.process(index_map, context)

    def test_dither_repr(self):
        """DitherStep repr includes config info."""
        config = DitherConfig(algorithm=DitherAlgorithm.ATKINSON, strength=0.8)
        step = DitherStep(config)

        repr_str = repr(step)

        assert "DitherStep" in repr_str
        assert "config=" in repr_str

    def test_dither_default_config(self):
        """DitherStep uses default config when None provided."""
        step = DitherStep()

        assert step.config is not None
        assert step.config.algorithm == DitherAlgorithm.FLOYD_STEINBERG
        assert step.config.order == ScanOrder.SERPENTINE
        assert step.config.strength == 1.0
