"""Comprehensive test suite for multi-size ICO generation.

This module provides pytest-based tests for the img2ico module with focus on:
- Multi-size ICO generation and format verification
- Deep validation of ICO file structure
- Image content integrity checking
- Different quality settings and compression
- Edge cases and error handling
- Cross-platform compatibility
- Performance and stress testing
"""

from __future__ import annotations

import hashlib
import io
import os
import struct
import sys
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Final

import pytest
from PIL import Image, ImageChops, ImageDraw, ImageFont

sys.path.insert(0, "pytola")

from pytola.office.img2ico.img2ico import ImageToIcoRunner

# Constants for test configuration
DEFAULT_TEST_IMAGE_SIZE: Final[int] = 256  # Reduced from 512 for faster testing
SMALL_TEST_IMAGE_SIZE: Final[int] = 128  # Even smaller for quick tests
DEFAULT_QUALITY_SETTINGS: Final[tuple[str, ...]] = ("low", "medium", "high")
DEFAULT_MULTI_SIZES: Final[set[int]] = {16, 32, 48, 64, 128}
LARGE_ICON_SIZES: Final[set[int]] = {128, 256}  # Reduced from {256, 512, 1024}
ALL_DEFAULT_SIZES: Final[set[int]] = {16, 24, 32, 48, 64, 128, 256}


@dataclass
class ICOEntry:
    """Represents a single ICO directory entry."""

    index: int
    width: int
    height: int
    raw_width: int
    raw_height: int
    color_count: int
    reserved: int
    planes: int
    bit_count: int
    size: int
    offset: int
    is_png: bool


@dataclass
class ICOImageData:
    """Represents ICO image data."""

    bytes: bytes
    hash: str
    is_valid_png: bool


@dataclass
class ICOParseResult:
    """Result of ICO file parsing."""

    header: tuple[int, int, int] | None
    entries: list[ICOEntry]
    image_data: list[ICOImageData]
    file_path: Path


class AdvancedICOVerifier:
    """Advanced ICO file verifier with deep structural analysis."""

    def __init__(self, ico_path: Path) -> None:
        self.ico_path = ico_path
        self.result: ICOParseResult = self._parse_ico()

    def _parse_ico(self) -> ICOParseResult:
        """Parse ICO file structure with detailed analysis."""
        try:
            with open(self.ico_path, "rb") as f:
                # Read ICO header
                header_data = f.read(6)
                header = struct.unpack("<HHH", header_data)

                reserved, ico_type, count = header

                if reserved != 0:
                    raise ValueError(f"Invalid ICO header reserved field: {reserved}")
                if ico_type != 1:
                    raise ValueError(f"Not an ICO file (type: {ico_type})")
                if count == 0:
                    raise ValueError("ICO file contains no images")

                entries: list[ICOEntry] = []
                image_data: list[ICOImageData] = []

                # Read directory entries
                for i in range(count):
                    entry_data = f.read(16)
                    (
                        width,
                        height,
                        color_count,
                        reserved_field,
                        planes,
                        bit_count,
                        size,
                        offset,
                    ) = struct.unpack("<BBBBHHII", entry_data)

                    # Convert width/height (0 means 256 or larger)
                    display_width = width if width > 0 else 256
                    display_height = height if height > 0 else 256

                    # Validate entry fields
                    if reserved_field != 0:
                        raise ValueError(f"Invalid reserved field in entry {i}: {reserved_field}")
                    if planes not in [0, 1]:
                        raise ValueError(f"Invalid planes value in entry {i}: {planes}")
                    if bit_count not in [1, 4, 8, 16, 24, 32]:
                        raise ValueError(f"Invalid bit_count in entry {i}: {bit_count}")
                    if size <= 0:
                        raise ValueError(f"Invalid image data size in entry {i}: {size}")
                    if offset < 6 + count * 16:
                        raise ValueError(f"Invalid offset in entry {i}: {offset}")

                    entry = ICOEntry(
                        index=i,
                        width=display_width,
                        height=display_height,
                        raw_width=width,
                        raw_height=height,
                        color_count=color_count,
                        reserved=reserved_field,
                        planes=planes,
                        bit_count=bit_count,
                        size=size,
                        offset=offset,
                        is_png=(color_count == 0),  # PNG format uses 0 color count
                    )

                    entries.append(entry)

                    # Read actual image data
                    current_pos = f.tell()
                    f.seek(offset)
                    image_bytes = f.read(size)
                    f.seek(current_pos)

                    image_datum = ICOImageData(
                        bytes=image_bytes,
                        hash=hashlib.md5(image_bytes).hexdigest(),
                        is_valid_png=self._is_valid_png(image_bytes) if entry.is_png else False,
                    )

                    image_data.append(image_datum)

                return ICOParseResult(
                    header=header,
                    entries=entries,
                    image_data=image_data,
                    file_path=self.ico_path,
                )

        except Exception as e:
            raise ValueError(f"Failed to parse ICO file: {e}") from e

    def _is_valid_png(self, data: bytes) -> bool:
        """Check if data is valid PNG format."""
        try:
            png_signature = b"\x89PNG\r\n\x1a\n"
            return data.startswith(png_signature)
        except Exception:
            return False

    def get_sizes(self) -> list[int]:
        """Get list of icon sizes."""
        return [entry.width for entry in self.result.entries]

    def verify_sizes(self, expected_sizes: set[int]) -> tuple[bool, dict[str, list[int]]]:
        """Verify that expected sizes are present."""
        actual_sizes = set(self.get_sizes())
        missing = expected_sizes - actual_sizes
        extra = actual_sizes - expected_sizes

        return actual_sizes == expected_sizes, {
            "missing": sorted(missing),
            "extra": sorted(extra),
            "actual": sorted(actual_sizes),
            "expected": sorted(expected_sizes),
        }

    def is_png_format(self) -> bool:
        """Check if all icons are in PNG format."""
        return all(entry.color_count == 0 for entry in self.result.entries)

    def verify_file_integrity(self) -> tuple[bool, list[str]]:
        """Verify file structure integrity."""
        issues = []

        # Check for overlapping data
        data_ranges = []
        for entry in self.result.entries:
            start = entry.offset
            end = start + entry.size
            data_ranges.append((start, end, entry.width))

        # Sort by offset
        data_ranges.sort()

        # Check for overlaps
        for i in range(len(data_ranges) - 1):
            current_end = data_ranges[i][1]
            next_start = data_ranges[i + 1][0]
            if current_end > next_start:
                issues.append(f"Overlapping data between sizes {data_ranges[i][2]} and {data_ranges[i + 1][2]}")

        # Check if all data fits in file
        file_size = self.result.file_path.stat().st_size
        max_end = max(end for _, end, _ in data_ranges)
        if max_end > file_size:
            issues.append(f"Data extends beyond file size: {max_end} > {file_size}")

        return len(issues) == 0, issues

    def verify_image_content(self, reference_image_path: Path) -> tuple[bool, dict[int, dict[str, Any]]]:
        """Verify that ICO images match the reference image content."""
        try:
            with Image.open(reference_image_path) as ref_img:
                if ref_img.mode != "RGBA":
                    ref_img = ref_img.convert("RGBA")

                results = {}
                for _i, (entry, img_data) in enumerate(zip(self.result.entries, self.result.image_data)):
                    size = entry.width
                    try:
                        # Extract image from ICO
                        if entry.is_png:
                            ico_img = Image.open(io.BytesIO(img_data.bytes))
                        else:
                            # For DIB format, we'd need more complex parsing
                            # For now, skip non-PNG verification
                            results[size] = {
                                "verified": False,
                                "reason": "DIB format not verified",
                            }
                            continue

                        # Resize reference to match ICO size
                        ref_resized = ref_img.resize((size, size), Image.Resampling.LANCZOS)

                        # Compare images
                        diff = ImageChops.difference(ico_img, ref_resized)
                        bbox = diff.getbbox()

                        results[size] = {
                            "verified": bbox is None,  # No difference if bbox is None
                            "difference_bbox": bbox,
                            "format": "PNG" if entry.is_png else "DIB",
                        }

                    except Exception as e:
                        results[size] = {"verified": False, "reason": str(e)}

                all_verified = all(result.get("verified", False) for result in results.values())
                return all_verified, results

        except Exception as e:
            return False, {"error": str(e)}

    def print_detailed_summary(self) -> None:
        """Print detailed ICO file analysis."""
        if not self.result.header:
            print("  [X] Failed to parse ICO file")
            return

        reserved, ico_type, count = self.result.header
        print(f"  ICO Header: Reserved={reserved}, Type={ico_type}, Count={count}")
        print(f"  Format: {'PNG' if self.is_png_format() else 'Mixed/DIB'}")
        print("  Embedded Images:")

        total_size = 0
        for i, (entry, img_data) in enumerate(zip(self.result.entries, self.result.image_data)):
            size_info = f"{entry.width}x{entry.height}"
            format_info = "PNG" if entry.is_png else f"DIB({entry.bit_count}bit)"
            print(
                f"    {i + 1}. {size_info:<8} - {format_info:<10} - "
                f"{len(img_data.bytes):,} bytes - "
                f"Offset: 0x{entry.offset:08x} - "
                f"MD5: {img_data.hash[:8]}..."
            )
            total_size += len(img_data.bytes)

        print(f"\n  Total image data: {total_size:,} bytes")
        print(f"  File size: {self.result.file_path.stat().st_size:,} bytes")

        # Integrity check
        integrity_ok, issues = self.verify_file_integrity()
        if integrity_ok:
            print("  ✓ File structure integrity: OK")
        else:
            print("  [X] File structure integrity issues:")
            for issue in issues:
                print(f"    - {issue}")


class ICOVerifier:
    """Verify ICO file structure and contents."""

    def __init__(self, ico_path: Path) -> None:
        self.ico_path = ico_path
        self.header: tuple[int, int, int] | None = None
        self.entries: list[dict[str, Any]] = []
        self._parse_ico()

    def _parse_ico(self) -> None:
        """Parse ICO file structure."""
        try:
            with open(self.ico_path, "rb") as f:
                # Read ICO header
                header_data = f.read(6)
                self.header = struct.unpack("<HHH", header_data)

                reserved, ico_type, count = self.header

                if ico_type != 1:
                    raise ValueError(f"Not an ICO file (type: {ico_type})")

                # Read directory entries
                for _ in range(count):
                    entry_data = f.read(16)
                    (
                        width,
                        height,
                        color_count,
                        reserved,
                        planes,
                        bit_count,
                        size,
                        offset,
                    ) = struct.unpack("<BBBBHHII", entry_data)

                    # Convert width/height (0 means 256 or larger)
                    display_width = width if width > 0 else 256
                    display_height = height if height > 0 else 256

                    self.entries.append(
                        {
                            "width": display_width,
                            "height": display_height,
                            "color_count": color_count,
                            "reserved": reserved,
                            "planes": planes,
                            "bit_count": bit_count,
                            "size": size,
                            "offset": offset,
                        }
                    )
        except Exception as e:
            raise ValueError(f"Failed to parse ICO file: {e}") from e

    def get_sizes(self) -> list[int]:
        """Get list of icon sizes."""
        return [entry["width"] for entry in self.entries]

    def verify_sizes(self, expected_sizes: set[int]) -> bool:
        """Verify that expected sizes are present."""
        actual_sizes = set(self.get_sizes())
        return actual_sizes == expected_sizes

    def is_png_format(self) -> bool:
        """Check if icons are in PNG format."""
        return all(entry["color_count"] == 0 for entry in self.entries)

    def print_summary(self) -> None:
        """Print ICO file summary."""
        if not self.header:
            print("  [X] Failed to parse ICO file")
            return

        _reserved, ico_type, count = self.header
        print(f"  ICO Header: Type={ico_type}, Count={count}")
        print(f"  Format: {'PNG' if self.is_png_format() else 'DIB'}")
        print("  Embedded Sizes:")

        for i, entry in enumerate(self.entries):
            print(
                f"    {i + 1}. {entry['width']}x{entry['height']} - "
                f"BitCount={entry['bit_count']}, "
                f"Size={entry['size']} bytes, "
                f"Offset={entry['offset']}"
            )


@dataclass
class ImageTestConfig:
    """Configuration for test image generation."""

    size: int = DEFAULT_TEST_IMAGE_SIZE
    path: Path | None = None
    grid_spacing: int = 16
    text_color: tuple[int, int, int, int] = (255, 0, 0, 255)
    grid_color: tuple[int, int, int, int] = (0, 0, 0, 255)


@dataclass
class ICOGenerationConfig:
    """Configuration for ICO generation testing."""

    input_path: Path
    output_path: Path
    sizes: set[int] | None = None
    quality: str = "high"

    def get_runner(self) -> ImageToIcoRunner:
        """Create ImageToIcoRunner instance with current configuration."""
        return ImageToIcoRunner(
            input_path=self.input_path,
            output_path=self.output_path,
            sizes=self.sizes,
            quality=self.quality,
        )


def create_test_image(config: ImageTestConfig | None = None) -> Path:
    """Create a test image with fine details for ICO generation testing.

    Args:
        config: Configuration for test image generation. If None, uses default settings.

    Returns
    -------
        Path to the created test image file.
    """
    if config is None:
        config = ImageTestConfig()

    if config.path is None:
        # Create in temporary directory
        temp_dir = tempfile.mkdtemp(prefix="img2ico_test_")
        config.path = Path(temp_dir) / f"test_image_{config.size}x{config.size}.png"

    path = config.path

    img = Image.new("RGBA", (config.size, config.size), color=(255, 255, 255, 255))
    draw = ImageDraw.Draw(img)

    # Draw fine details for better visual verification
    for i in range(0, config.size, config.grid_spacing):
        draw.line([(i, 0), (i, config.size)], fill=config.grid_color, width=1)
        draw.line([(0, i), (config.size, i)], fill=config.grid_color, width=1)

    # Add identifying text
    try:
        font = ImageFont.truetype("arial.ttf", config.size // 16)
    except Exception:
        font = ImageFont.load_default()
    draw.text(
        (config.size // 4, config.size // 4),
        f"Test {config.size}x{config.size}",
        fill=config.text_color,
        font=font,
    )

    img.save(path)
    return path


# Pytest fixtures for test resource management
@pytest.fixture(scope="session")
def temp_test_dir() -> Path:
    """Create temporary directory for test files (session scope for better performance)."""
    with tempfile.TemporaryDirectory(prefix="img2ico_test_") as tmpdir:
        yield Path(tmpdir)


@pytest.fixture(scope="session")
def sample_test_image(temp_test_dir: Path) -> Path:
    """Create a sample test image for testing (session scope for reuse)."""
    config = ImageTestConfig(size=DEFAULT_TEST_IMAGE_SIZE, path=temp_test_dir / "sample_test.png")
    return create_test_image(config)


@pytest.fixture
def multi_size_config(sample_test_image: Path, temp_test_dir: Path) -> ICOGenerationConfig:
    """Create configuration for multi-size ICO generation."""
    return ICOGenerationConfig(
        input_path=sample_test_image,
        output_path=temp_test_dir / f"multi_size_test_{id(multi_size_config)}.ico",
        sizes=DEFAULT_MULTI_SIZES,
        quality="medium",  # Reduced from high for faster processing
    )


# Standard pytest test class
class TestMultiSizeICOGeneration:
    """Test suite for multi-size ICO generation functionality."""

    def test_basic_multi_size_generation(self, multi_size_config: ICOGenerationConfig) -> None:
        """Test basic multi-size ICO generation with advanced verification."""
        runner = multi_size_config.get_runner()
        runner.run()

        # Verify ICO file was created
        assert multi_size_config.output_path.exists(), "ICO file should be created"

        # Advanced verification
        verifier = AdvancedICOVerifier(multi_size_config.output_path)

        # Size verification
        size_match, size_details = verifier.verify_sizes(DEFAULT_MULTI_SIZES)
        assert size_match, f"Expected sizes {DEFAULT_MULTI_SIZES}, got {size_details['actual']}"

        # Format verification
        assert verifier.is_png_format(), "All icons should be in PNG format"

        # Integrity verification
        integrity_ok, issues = verifier.verify_file_integrity()
        assert integrity_ok, f"File integrity issues: {issues}"

    def test_all_default_sizes_generation(self, sample_test_image: Path, temp_test_dir: Path) -> None:
        """Test ICO generation with all default sizes."""
        config = ICOGenerationConfig(
            input_path=sample_test_image,
            output_path=temp_test_dir / "all_sizes_test.ico",
            quality="high",
        )

        runner = config.get_runner()
        runner.run()

        assert config.output_path.exists(), "ICO file should be created"

        verifier = ICOVerifier(config.output_path)
        actual_sizes = set(verifier.get_sizes())

        # Should contain most default sizes
        assert len(actual_sizes & ALL_DEFAULT_SIZES) >= len(ALL_DEFAULT_SIZES) - 2, (
            f"Should contain most default sizes. Got: {sorted(actual_sizes)}"
        )

    @pytest.mark.parametrize("quality", DEFAULT_QUALITY_SETTINGS)
    def test_quality_settings(self, sample_test_image: Path, temp_test_dir: Path, quality: str) -> None:
        """Test different quality settings produce valid ICO files."""
        config = ICOGenerationConfig(
            input_path=sample_test_image,
            output_path=temp_test_dir / f"quality_{quality}_test.ico",
            sizes={16, 32, 64, 128},
            quality=quality,
        )

        runner = config.get_runner()
        runner.run()

        assert config.output_path.exists(), f"ICO file with {quality} quality should be created"

        verifier = ICOVerifier(config.output_path)
        assert verifier.verify_sizes(config.sizes or DEFAULT_MULTI_SIZES), f"Sizes should match for {quality} quality"

    @pytest.mark.parametrize(
        ("sizes", "test_name"),
        [
            ({32}, "single_size"),
            ({16, 256}, "two_sizes"),
            ({24, 48}, "non_square_sizes"),
        ],
    )
    def test_edge_cases(
        self,
        sample_test_image: Path,
        temp_test_dir: Path,
        sizes: set[int],
        test_name: str,
    ) -> None:
        """Test edge cases for ICO generation."""
        config = ICOGenerationConfig(
            input_path=sample_test_image,
            output_path=temp_test_dir / f"edge_{test_name}_test.ico",
            sizes=sizes,
            quality="high",
        )

        runner = config.get_runner()
        runner.run()

        assert config.output_path.exists(), f"ICO file for {test_name} should be created"

        verifier = ICOVerifier(config.output_path)
        assert verifier.verify_sizes(sizes), f"Sizes should match for {test_name}"

    def test_large_icon_sizes(self, sample_test_image: Path, temp_test_dir: Path) -> None:
        """Test generation of large icon sizes."""
        # Use existing sample image instead of creating new large image
        config = ICOGenerationConfig(
            input_path=sample_test_image,
            output_path=temp_test_dir / "large_sizes_test.ico",
            sizes=LARGE_ICON_SIZES,
            quality="medium",  # Reduced quality for speed
        )

        runner = config.get_runner()
        runner.run()

        assert config.output_path.exists(), "Large size ICO file should be created"

        verifier = ICOVerifier(config.output_path)
        actual_sizes = set(verifier.get_sizes())
        # Allow some flexibility - should contain at least 1 of the large sizes
        common_sizes = actual_sizes & LARGE_ICON_SIZES
        assert len(common_sizes) >= 1, f"Should contain at least 1 large size. Got: {sorted(actual_sizes)}"

    def test_image_content_verification(self, multi_size_config: ICOGenerationConfig) -> None:
        """Test that ICO images match the reference image content."""
        runner = multi_size_config.get_runner()
        runner.run()

        assert multi_size_config.output_path.exists(), "ICO file should be created"

        verifier = AdvancedICOVerifier(multi_size_config.output_path)
        _content_match, content_results = verifier.verify_image_content(multi_size_config.input_path)

        # At least some sizes should match (PNG format verification)
        verified_count = sum(1 for result in content_results.values() if result.get("verified", False))
        total_count = len(content_results)

        # Allow some failures due to format differences
        assert verified_count >= total_count * 0.5, (
            f"At least half of the sizes should match content. Verified: {verified_count}/{total_count}"
        )

    def test_error_handling_invalid_input(self, temp_test_dir: Path) -> None:
        """Test error handling for invalid input files."""
        invalid_file = temp_test_dir / "nonexistent.png"
        config = ICOGenerationConfig(input_path=invalid_file, output_path=temp_test_dir / "should_fail.ico")

        runner = config.get_runner()

        # The img2ico module may not raise exceptions for missing files
        # but should handle the error gracefully
        try:
            runner.run()
            # If it doesn't raise an exception, the output file should not exist
            assert not config.output_path.exists(), "Output file should not be created for invalid input"
        except Exception:
            # If it does raise an exception, that's also acceptable
            pass

    def test_verifier_print_summary(self, multi_size_config: ICOGenerationConfig, capsys) -> None:
        """Test that verifier summary printing works correctly."""
        runner = multi_size_config.get_runner()
        runner.run()

        verifier = AdvancedICOVerifier(multi_size_config.output_path)
        verifier.print_detailed_summary()

        captured = capsys.readouterr()
        assert "ICO Header" in captured.out
        assert "Embedded Images" in captured.out


class TestBenchmarkICOGeneration:
    """Benchmark tests for ICO generation performance."""

    @pytest.mark.benchmark(group="ico_generation")
    def test_benchmark_basic_generation(self, benchmark, sample_test_image: Path, temp_test_dir: Path) -> None:
        """Benchmark basic ICO generation performance."""
        config = ICOGenerationConfig(
            input_path=sample_test_image,
            output_path=temp_test_dir / "benchmark_test.ico",
            sizes={16, 32, 64},
        )

        def generate_ico():
            runner = config.get_runner()
            runner.run()
            return config.output_path.exists()

        result = benchmark(generate_ico)
        assert result, "Benchmark generation should succeed"

    @pytest.mark.benchmark(group="ico_generation_large")
    def test_benchmark_large_generation(self, benchmark, sample_test_image: Path, temp_test_dir: Path) -> None:
        """Benchmark large ICO generation performance."""
        config = ICOGenerationConfig(
            input_path=sample_test_image,
            output_path=temp_test_dir / "benchmark_large_test.ico",
            sizes=ALL_DEFAULT_SIZES,
        )

        def generate_large_ico():
            runner = config.get_runner()
            runner.run()
            return config.output_path.exists()

        result = benchmark(generate_large_ico)
        assert result, "Large benchmark generation should succeed"


# Legacy compatibility functions (for existing test scripts)
def test_basic_multi_size(tmp_path: Path) -> None:
    """Legacy compatibility function for basic multi-size test."""
    try:
        config = ImageTestConfig(size=512, path=tmp_path / "test_basic.png")
        test_image = create_test_image(config)

        ico_config = ICOGenerationConfig(
            input_path=test_image,
            output_path=tmp_path / "test_basic_multi.ico",
            sizes={16, 32, 48, 64, 128, 256},
            quality="high",
        )

        runner = ico_config.get_runner()
        runner.run()

        verifier = AdvancedICOVerifier(ico_config.output_path)
        size_match, _ = verifier.verify_sizes(ico_config.sizes)
        assert size_match, "Basic multi-size ICO generation should succeed"
    except Exception as e:
        pytest.fail(f"Basic multi-size test failed: {e}")


# Main execution for backward compatibility
if __name__ == "__main__":
    # Run pytest programmatically
    import subprocess

    result = subprocess.run([sys.executable, "-m", "pytest", __file__, "-v"], cwd=os.path.dirname(__file__))
    sys.exit(result.returncode)
