"""
Tests for overview_jpeg_tools package.

Copyright (c) 2024-2025 Overview Corporation. All rights reserved.
"""

from io import BytesIO
from pathlib import Path

import pytest

from overview_jpeg_tools import JpegJsonReader, MetadataDecodingError, NoMetadataError


# Path to test fixtures
FIXTURES_DIR = Path(__file__).parent / "fixtures"
TEST_IMAGE = FIXTURES_DIR / "353359.jpg"


class TestJpegJsonReader:
    """Tests for JpegJsonReader class."""

    def test_load_from_file_path_string(self):
        """Test loading JPEG from file path as string."""
        reader = JpegJsonReader(str(TEST_IMAGE))
        assert reader.source_path == TEST_IMAGE
        assert len(reader.image_data) > 0

    def test_load_from_file_path_pathlib(self):
        """Test loading JPEG from pathlib.Path."""
        reader = JpegJsonReader(TEST_IMAGE)
        assert reader.source_path == TEST_IMAGE
        assert len(reader.image_data) > 0

    def test_load_from_bytes(self):
        """Test loading JPEG from bytes."""
        with open(TEST_IMAGE, "rb") as f:
            image_bytes = f.read()
        
        reader = JpegJsonReader(image_bytes)
        assert reader.source_path is None
        assert reader.image_data == image_bytes

    def test_load_from_bytesio(self):
        """Test loading JPEG from BytesIO."""
        with open(TEST_IMAGE, "rb") as f:
            image_bytes = f.read()
        
        reader = JpegJsonReader(BytesIO(image_bytes))
        assert reader.source_path is None
        assert reader.image_data == image_bytes

    def test_has_embedded_json(self):
        """Test detection of embedded JSON data."""
        reader = JpegJsonReader(TEST_IMAGE)
        assert reader.has_embedded_json() is True

    def test_extract_json(self):
        """Test extraction of embedded JSON data."""
        reader = JpegJsonReader(TEST_IMAGE)
        data = reader.extract_json()
        
        assert data is not None
        assert isinstance(data, dict)
        
        # Verify expected keys are present
        expected_keys = [
            "schema_version",
            "results",
            "pass_fail",
            "should_save",
            "capture_metadata",
            "system_name",
            "system_software_version",
            "system_serial_number",
            "system_model",
            "recipe_name",
        ]
        for key in expected_keys:
            assert key in data, f"Expected key '{key}' not found in extracted JSON"

    def test_extract_json_caching(self):
        """Test that extracted JSON is cached for subsequent calls."""
        reader = JpegJsonReader(TEST_IMAGE)
        
        # First extraction
        data1 = reader.extract_json()
        # Second extraction should return cached data
        data2 = reader.extract_json()
        
        assert data1 is data2  # Should be the same object (cached)

    def test_to_bytes(self):
        """Test to_bytes() returns the image data."""
        reader = JpegJsonReader(TEST_IMAGE)
        
        assert reader.to_bytes() == reader.image_data

    def test_invalid_jpeg_raises_error(self):
        """Test that invalid JPEG data raises ValueError."""
        invalid_data = b"not a jpeg file"
        
        with pytest.raises(ValueError, match="Not a valid JPEG"):
            JpegJsonReader(invalid_data)

    def test_file_not_found_raises_error(self):
        """Test that non-existent file raises FileNotFoundError."""
        with pytest.raises(FileNotFoundError):
            JpegJsonReader("/nonexistent/path/to/image.jpg")

    def test_invalid_source_type_raises_error(self):
        """Test that invalid source type raises ValueError."""
        with pytest.raises(ValueError, match="Source must be file path"):
            JpegJsonReader(12345)  # type: ignore


class TestJpegJsonReaderNoJson:
    """Tests for JpegJsonReader with images that have no embedded JSON."""

    def test_has_embedded_json_false_for_plain_jpeg(self):
        """Test that plain JPEG without embedded JSON returns False."""
        # Create a minimal valid JPEG (just SOI and EOI markers)
        # This is technically valid but has no content
        minimal_jpeg = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xd9'
        
        reader = JpegJsonReader(minimal_jpeg)
        assert reader.has_embedded_json() is False

    def test_extract_json_returns_none_for_plain_jpeg(self):
        """Test that plain JPEG without embedded JSON raises NoMetadataError."""
        minimal_jpeg = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xd9'
        
        reader = JpegJsonReader(minimal_jpeg)
        with pytest.raises(NoMetadataError, match="no JSON metadata"):
            reader.extract_json()


class TestJpegJsonReaderExceptions:
    """Tests for JpegJsonReader exception handling."""

    def test_no_metadata_error_raised_when_no_json_segments(self):
        """Test that NoMetadataError is raised when image has no JSON segments."""
        minimal_jpeg = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xd9'
        reader = JpegJsonReader(minimal_jpeg)
        
        with pytest.raises(NoMetadataError) as exc_info:
            reader.extract_json()
        
        assert "json metadata" in str(exc_info.value).lower()

    def test_metadata_decoding_error_raised_on_incomplete_chunks(self):
        """Test that MetadataDecodingError is raised when chunks are incomplete."""
        import struct
        
        # Create a JPEG with our JSON marker but incomplete chunk data
        # This will have a segment that starts correctly but has incomplete chunks
        header = b'JSON\x00\x00'
        # Chunk 0 of 2 (but we won't add chunk 1, making it incomplete)
        chunk_info = struct.pack('>HH', 0, 2)  # index=0, total=2
        chunk_data = b'incomplete'
        
        segment_data = header + chunk_info + chunk_data
        segment_size = len(segment_data) + 2  # +2 for the size field itself
        
        # Build a minimal JPEG with APP1 marker
        jpeg_data = (
            b'\xff\xd8'  # SOI
            b'\xff\xe1'  # APP1 marker
            + struct.pack('>H', segment_size)
            + segment_data
            + b'\xff\xd9'  # EOI
        )
        
        reader = JpegJsonReader(jpeg_data)
        
        with pytest.raises(MetadataDecodingError) as exc_info:
            reader.extract_json()
        
        assert "incomplete" in str(exc_info.value).lower()

    def test_metadata_decoding_error_raised_on_decompression_failure(self):
        """Test that MetadataDecodingError is raised when decompression fails."""
        import struct
        
        # Create a JPEG with our JSON marker and valid chunks but invalid compressed data
        header = b'JSON\x00\x00'
        chunk_info = struct.pack('>HH', 0, 1)  # Single chunk
        chunk_data = b'not valid zlib data'
        
        segment_data = header + chunk_info + chunk_data
        segment_size = len(segment_data) + 2
        
        jpeg_data = (
            b'\xff\xd8'
            b'\xff\xe1'
            + struct.pack('>H', segment_size)
            + segment_data
            + b'\xff\xd9'
        )
        
        reader = JpegJsonReader(jpeg_data)
        
        with pytest.raises(MetadataDecodingError) as exc_info:
            reader.extract_json()
        
        # Should mention decompression or parsing failure
        error_msg = str(exc_info.value).lower()
        assert "decompress" in error_msg or "parse" in error_msg
