"""
Tests for media upload functionality (v1.2.0 Phase 3)

Tests media upload, validation, optimization, and display per ADR-057 and ADR-058.
Uses generated test images (PIL Image.new()) per Q31.
"""

import pytest
from PIL import Image
import io
from pathlib import Path
from datetime import datetime

from starpunk.media import (
    validate_image,
    optimize_image,
    save_media,
    attach_media_to_note,
    get_note_media,
    delete_media,
    MAX_FILE_SIZE,
    MAX_DIMENSION,
    RESIZE_DIMENSION,
    MAX_IMAGES_PER_NOTE,
    HEIC_SUPPORTED,
)


def create_test_image(width=800, height=600, format='PNG'):
    """
    Generate test image using PIL

    Per Q31: Use generated test images, not real files

    Args:
        width: Image width in pixels
        height: Image height in pixels
        format: Image format (PNG, JPEG, GIF, WEBP)

    Returns:
        Bytes of image data
    """
    img = Image.new('RGB', (width, height), color='red')
    buffer = io.BytesIO()
    img.save(buffer, format=format)
    buffer.seek(0)
    return buffer.getvalue()


def create_test_heic(width=800, height=600):
    """
    Generate test HEIC image using pillow-heif

    Args:
        width: Image width in pixels
        height: Image height in pixels

    Returns:
        Bytes of HEIC image data
    """
    if not HEIC_SUPPORTED:
        pytest.skip("pillow-heif not available")

    import pillow_heif

    # Create a simple RGB image
    img = Image.new('RGB', (width, height), color='blue')

    # Convert to HEIC
    buffer = io.BytesIO()
    heif_file = pillow_heif.from_pillow(img)
    heif_file.save(buffer, format='HEIF')
    buffer.seek(0)
    return buffer.getvalue()


def create_test_mpo(width=800, height=600):
    """
    Generate test MPO (Multi-Picture Object) image

    MPO format is used by iPhones for depth/portrait photos. It contains
    multiple JPEG images in one file. For testing, we create a simple MPO
    with a single frame.

    Args:
        width: Image width in pixels
        height: Image height in pixels

    Returns:
        Bytes of MPO image data
    """
    # Create a simple RGB image
    img = Image.new('RGB', (width, height), color='green')

    # Save as MPO (Pillow will handle this correctly)
    buffer = io.BytesIO()
    img.save(buffer, format='MPO', quality=95)
    buffer.seek(0)
    return buffer.getvalue()


class TestImageValidation:
    """Test validate_image function"""

    def test_valid_jpeg(self):
        """Test validation of valid JPEG image"""
        image_data = create_test_image(800, 600, 'JPEG')
        file_data, mime_type, width, height = validate_image(image_data, 'test.jpg')

        assert mime_type == 'image/jpeg'
        assert width == 800
        assert height == 600
        assert file_data == image_data  # No conversion

    def test_valid_png(self):
        """Test validation of valid PNG image"""
        image_data = create_test_image(800, 600, 'PNG')
        file_data, mime_type, width, height = validate_image(image_data, 'test.png')

        assert mime_type == 'image/png'
        assert width == 800
        assert height == 600
        assert file_data == image_data  # No conversion

    def test_valid_gif(self):
        """Test validation of valid GIF image"""
        image_data = create_test_image(800, 600, 'GIF')
        file_data, mime_type, width, height = validate_image(image_data, 'test.gif')

        assert mime_type == 'image/gif'
        assert width == 800
        assert height == 600
        assert file_data == image_data  # No conversion

    def test_valid_webp(self):
        """Test validation of valid WebP image"""
        image_data = create_test_image(800, 600, 'WEBP')
        file_data, mime_type, width, height = validate_image(image_data, 'test.webp')

        assert mime_type == 'image/webp'
        assert width == 800
        assert height == 600
        assert file_data == image_data  # No conversion

    def test_file_too_large(self):
        """Test rejection of >10MB file (per Q6)"""
        # Create data larger than MAX_FILE_SIZE
        large_data = b'x' * (MAX_FILE_SIZE + 1)

        with pytest.raises(ValueError) as exc_info:
            validate_image(large_data, 'large.jpg')

        assert "File too large" in str(exc_info.value)

    def test_dimensions_too_large(self):
        """Test rejection of >12000px image (v1.4.2: increased from 4096)"""
        large_image = create_test_image(13000, 13000, 'PNG')

        with pytest.raises(ValueError) as exc_info:
            validate_image(large_image, 'huge.png')

        assert "dimensions too large" in str(exc_info.value).lower()

    def test_corrupted_image(self):
        """Test rejection of corrupted image data"""
        corrupted_data = b'not an image'

        with pytest.raises(ValueError) as exc_info:
            validate_image(corrupted_data, 'corrupt.jpg')

        assert "Invalid or corrupted" in str(exc_info.value)


class TestHEICSupport:
    """Test HEIC/HEIF image format support (v1.4.2)"""

    def test_heic_detection_and_conversion(self):
        """Test HEIC file is detected and converted to JPEG"""
        heic_data = create_test_heic(800, 600)
        file_data, mime_type, width, height = validate_image(heic_data, 'test.heic')

        # Should be converted to JPEG
        assert mime_type == 'image/jpeg'
        assert width == 800
        assert height == 600
        # file_data should be different (converted to JPEG)
        assert file_data != heic_data
        # Verify it's actually JPEG by opening it
        img = Image.open(io.BytesIO(file_data))
        assert img.format == 'JPEG'

    def test_heic_with_rgba_mode(self):
        """Test HEIC with alpha channel is converted to RGB JPEG"""
        if not HEIC_SUPPORTED:
            pytest.skip("pillow-heif not available")

        import pillow_heif

        # Create image with alpha channel
        img = Image.new('RGBA', (800, 600), color=(255, 0, 0, 128))
        buffer = io.BytesIO()
        heif_file = pillow_heif.from_pillow(img)
        heif_file.save(buffer, format='HEIF')
        buffer.seek(0)
        heic_data = buffer.getvalue()

        file_data, mime_type, width, height = validate_image(heic_data, 'test.heic')

        # Should be converted to JPEG (no alpha)
        assert mime_type == 'image/jpeg'
        # Verify it's RGB (no alpha)
        img = Image.open(io.BytesIO(file_data))
        assert img.mode == 'RGB'

    def test_heic_dimensions_preserved(self):
        """Test HEIC conversion preserves dimensions"""
        heic_data = create_test_heic(1024, 768)
        file_data, mime_type, width, height = validate_image(heic_data, 'photo.heic')

        assert width == 1024
        assert height == 768
        assert mime_type == 'image/jpeg'

    def test_heic_error_without_library(self, monkeypatch):
        """Test appropriate error when HEIC uploaded but pillow-heif not available"""
        # Mock HEIC_SUPPORTED to False
        from starpunk import media
        monkeypatch.setattr(media, 'HEIC_SUPPORTED', False)

        # Create a mock HEIC file (just needs to be recognized as HEIC by Pillow)
        # We'll create a real HEIC if library is available, otherwise skip
        if not HEIC_SUPPORTED:
            pytest.skip("pillow-heif not available to create test HEIC")

        heic_data = create_test_heic(800, 600)

        with pytest.raises(ValueError) as exc_info:
            validate_image(heic_data, 'test.heic')

        assert "HEIC/HEIF images require pillow-heif library" in str(exc_info.value)
        assert "convert to JPEG" in str(exc_info.value)

    def test_heic_full_upload_flow(self, app):
        """Test complete HEIC upload through save_media"""
        heic_data = create_test_heic(800, 600)

        with app.app_context():
            media_info = save_media(heic_data, 'iphone_photo.heic')

            # Should be saved as JPEG
            assert media_info['mime_type'] == 'image/jpeg'
            assert media_info['width'] == 800
            assert media_info['height'] == 600

            # Verify file was saved
            media_path = Path(app.config['DATA_PATH']) / 'media' / media_info['path']
            assert media_path.exists()

            # Verify it's actually a JPEG file
            saved_img = Image.open(media_path)
            assert saved_img.format == 'JPEG'


class TestMPOSupport:
    """Test MPO (Multi-Picture Object) image format support (v1.5.0 Phase 5)"""

    def test_mpo_detection_and_conversion(self):
        """Test MPO file is detected and converted to JPEG"""
        mpo_data = create_test_mpo(800, 600)
        file_data, mime_type, width, height = validate_image(mpo_data, 'test.mpo')

        # Should be converted to JPEG
        assert mime_type == 'image/jpeg'
        assert width == 800
        assert height == 600
        # Verify it's actually JPEG by opening it
        img = Image.open(io.BytesIO(file_data))
        assert img.format == 'JPEG'
        # Note: MPO with single frame may have identical bytes to JPEG
        # The important part is the format detection and MIME type correction

    def test_mpo_dimensions_preserved(self):
        """Test MPO conversion preserves dimensions"""
        mpo_data = create_test_mpo(1024, 768)
        file_data, mime_type, width, height = validate_image(mpo_data, 'photo.mpo')

        assert width == 1024
        assert height == 768
        assert mime_type == 'image/jpeg'

    def test_mpo_full_upload_flow(self, app):
        """Test complete MPO upload through save_media"""
        mpo_data = create_test_mpo(800, 600)

        with app.app_context():
            media_info = save_media(mpo_data, 'iphone_portrait.mpo')

            # Should be saved as JPEG
            assert media_info['mime_type'] == 'image/jpeg'
            assert media_info['width'] == 800
            assert media_info['height'] == 600

            # Verify file was saved
            media_path = Path(app.config['DATA_PATH']) / 'media' / media_info['path']
            assert media_path.exists()

            # Verify it's actually a JPEG file
            saved_img = Image.open(media_path)
            assert saved_img.format == 'JPEG'


class TestImageOptimization:
    """Test optimize_image function"""

    def test_no_resize_needed(self):
        """Test image within limits is not resized"""
        image_data = create_test_image(1024, 768, 'PNG')
        optimized, width, height, optimized_bytes = optimize_image(image_data)

        assert width == 1024
        assert height == 768
        assert optimized_bytes is not None
        assert len(optimized_bytes) > 0

    def test_resize_large_image(self):
        """Test auto-resize of >2048px image (per ADR-058)"""
        large_image = create_test_image(3000, 2000, 'PNG')
        optimized, width, height, optimized_bytes = optimize_image(large_image)

        # Should be resized to 2048px on longest edge
        assert width == RESIZE_DIMENSION
        # Height should be proportionally scaled
        assert height == int(2000 * (RESIZE_DIMENSION / 3000))
        assert optimized_bytes is not None

    def test_aspect_ratio_preserved(self):
        """Test aspect ratio is maintained during resize"""
        image_data = create_test_image(3000, 1500, 'PNG')
        optimized, width, height, optimized_bytes = optimize_image(image_data)

        # Original aspect ratio: 2:1
        # After resize: should still be 2:1
        assert width / height == pytest.approx(2.0, rel=0.01)
        assert optimized_bytes is not None

    def test_gif_animation_preserved(self):
        """Test GIF animation preservation (per Q12)"""
        # For v1.2.0: Just verify GIF is handled without error
        # Full animation preservation is complex
        gif_data = create_test_image(800, 600, 'GIF')
        optimized, width, height, optimized_bytes = optimize_image(gif_data)

        assert width > 0
        assert height > 0
        assert optimized_bytes is not None


class TestMediaSave:
    """Test save_media function"""

    def test_save_valid_image(self, app):
        """Test saving valid image"""
        image_data = create_test_image(800, 600, 'PNG')

        with app.app_context():
            media_info = save_media(image_data, 'test.png')

            assert media_info['id'] > 0
            assert media_info['filename'] == 'test.png'
            assert media_info['mime_type'] == 'image/png'
            assert media_info['width'] == 800
            assert media_info['height'] == 600
            assert media_info['size'] > 0

            # Check file was created
            media_path = Path(app.config['DATA_PATH']) / 'media' / media_info['path']
            assert media_path.exists()

    def test_uuid_filename(self, app):
        """Test UUID-based filename generation (per Q5)"""
        image_data = create_test_image(800, 600, 'PNG')

        with app.app_context():
            media_info = save_media(image_data, 'original-name.png')

            # Stored filename should be different from original
            assert media_info['stored_filename'] != 'original-name.png'
            # Should end with .png
            assert media_info['stored_filename'].endswith('.png')
            # Path should be YYYY/MM/uuid.ext (per Q2)
            parts = media_info['path'].split('/')
            assert len(parts) == 3  # year/month/filename
            assert len(parts[0]) == 4  # Year
            assert len(parts[1]) == 2  # Month

    def test_auto_resize_on_save(self, app):
        """Test image >2048px is automatically resized"""
        large_image = create_test_image(3000, 2000, 'PNG')

        with app.app_context():
            media_info = save_media(large_image, 'large.png')

            # Should be resized
            assert media_info['width'] == RESIZE_DIMENSION
            assert media_info['height'] < 2000


class TestMediaAttachment:
    """Test attach_media_to_note function"""

    def test_attach_single_image(self, app, sample_note):
        """Test attaching single image to note"""
        image_data = create_test_image(800, 600, 'PNG')

        with app.app_context():
            # Save media
            media_info = save_media(image_data, 'test.png')

            # Attach to note
            attach_media_to_note(sample_note.id, [media_info['id']], ['Test caption'])

            # Verify attachment
            media_list = get_note_media(sample_note.id)
            assert len(media_list) == 1
            assert media_list[0]['id'] == media_info['id']
            assert media_list[0]['caption'] == 'Test caption'
            assert media_list[0]['display_order'] == 0

    def test_attach_multiple_images(self, app, sample_note):
        """Test attaching multiple images (up to 4)"""
        with app.app_context():
            media_ids = []
            captions = []

            for i in range(4):
                image_data = create_test_image(800, 600, 'PNG')
                media_info = save_media(image_data, f'test{i}.png')
                media_ids.append(media_info['id'])
                captions.append(f'Caption {i}')

            attach_media_to_note(sample_note.id, media_ids, captions)

            media_list = get_note_media(sample_note.id)
            assert len(media_list) == 4

            # Verify order
            for i, media_item in enumerate(media_list):
                assert media_item['display_order'] == i
                assert media_item['caption'] == f'Caption {i}'

    def test_reject_more_than_4_images(self, app, sample_note):
        """Test rejection of 5th image (per Q6)"""
        with app.app_context():
            media_ids = []
            captions = []

            for i in range(5):
                image_data = create_test_image(800, 600, 'PNG')
                media_info = save_media(image_data, f'test{i}.png')
                media_ids.append(media_info['id'])
                captions.append('')

            with pytest.raises(ValueError) as exc_info:
                attach_media_to_note(sample_note.id, media_ids, captions)

            assert "Maximum 4 images" in str(exc_info.value)

    def test_optional_captions(self, app, sample_note):
        """Test captions are optional (per Q7)"""
        image_data = create_test_image(800, 600, 'PNG')

        with app.app_context():
            media_info = save_media(image_data, 'test.png')

            # Attach without caption
            attach_media_to_note(sample_note.id, [media_info['id']], [''])

            media_list = get_note_media(sample_note.id)
            assert media_list[0]['caption'] is None or media_list[0]['caption'] == ''


class TestMediaDeletion:
    """Test delete_media function"""

    def test_delete_media_file(self, app):
        """Test deletion of media file and record"""
        image_data = create_test_image(800, 600, 'PNG')

        with app.app_context():
            media_info = save_media(image_data, 'test.png')
            media_id = media_info['id']
            media_path = Path(app.config['DATA_PATH']) / 'media' / media_info['path']

            # Verify file exists
            assert media_path.exists()

            # Delete media
            delete_media(media_id)

            # Verify file deleted
            assert not media_path.exists()

    def test_delete_orphaned_associations(self, app, sample_note):
        """Test cascade deletion of note_media associations"""
        image_data = create_test_image(800, 600, 'PNG')

        with app.app_context():
            media_info = save_media(image_data, 'test.png')
            attach_media_to_note(sample_note.id, [media_info['id']], ['Test'])

            # Delete media
            delete_media(media_info['id'])

            # Verify association also deleted
            media_list = get_note_media(sample_note.id)
            assert len(media_list) == 0


class TestMediaSecurityEscaping:
    """Test HTML/JavaScript escaping in media display (per media-display-fixes.md)"""

    def test_caption_html_escaped_in_alt_attribute(self, app, sample_note):
        """
        Test that captions containing HTML are properly escaped in alt attributes

        Per media-display-fixes.md Security Considerations:
        "Alt text must be HTML-escaped in templates"

        This prevents XSS attacks via malicious caption content.
        """
        from starpunk.media import attach_media_to_note, save_media

        image_data = create_test_image(800, 600, 'PNG')

        # Create caption with HTML tags that should be escaped
        malicious_caption = '<script>alert("XSS")</script><img src=x onerror=alert(1)>'

        with app.app_context():
            # Save media with malicious caption
            media_info = save_media(image_data, 'test.png')
            attach_media_to_note(sample_note.id, [media_info['id']], [malicious_caption])

            # Get the rendered note page
            client = app.test_client()
            response = client.get(f'/note/{sample_note.slug}')

            assert response.status_code == 200

            # Verify the HTML is escaped in the alt attribute
            # The caption should appear as escaped HTML entities, not raw HTML
            html = response.data.decode('utf-8')

            # Should NOT contain unescaped HTML tags
            assert '<script>alert("XSS")</script>' not in html
            assert '<img src=x onerror=alert(1)>' not in html

            # Should NOT have onerror as an actual HTML attribute (i.e., outside quotes)
            # Pattern: onerror= followed by something that isn't part of an alt value
            assert 'onerror=' not in html or 'alt=' in html.split('onerror=')[0]

            # Should contain escaped versions (Jinja2 auto-escapes by default)
            # The HTML tags should be escaped
            assert '&lt;script&gt;' in html
            assert '&lt;img' in html

    def test_caption_quotes_escaped_in_alt_attribute(self, app, sample_note):
        """
        Test that captions containing quotes are properly escaped in alt attributes

        This prevents breaking out of the alt attribute with malicious quotes.
        """
        from starpunk.media import attach_media_to_note, save_media

        image_data = create_test_image(800, 600, 'PNG')

        # Create caption with quotes that could break alt attribute
        caption_with_quotes = 'Image" onload="alert(\'XSS\')'

        with app.app_context():
            # Save media with caption containing quotes
            media_info = save_media(image_data, 'test.png')
            attach_media_to_note(sample_note.id, [media_info['id']], [caption_with_quotes])

            # Get the rendered note page
            client = app.test_client()
            response = client.get(f'/note/{sample_note.slug}')

            assert response.status_code == 200

            html = response.data.decode('utf-8')

            # Should NOT contain unescaped onload event
            assert 'onload="alert' not in html

            # The quote should be properly escaped
            # Jinja2 should escape quotes in attributes
            assert '&#34;' in html or '&quot;' in html or '&#39;' in html

    def test_caption_displayed_on_homepage(self, app, sample_note):
        """
        Test that media with captions are properly escaped on homepage too

        Per media-display-fixes.md, homepage also displays media using the same macro.
        """
        from starpunk.media import attach_media_to_note, save_media

        image_data = create_test_image(800, 600, 'PNG')
        malicious_caption = '<img src=x onerror=alert(1)>'

        with app.app_context():
            media_info = save_media(image_data, 'test.png')
            attach_media_to_note(sample_note.id, [media_info['id']], [malicious_caption])

            # Get the homepage
            client = app.test_client()
            response = client.get('/')

            assert response.status_code == 200

            html = response.data.decode('utf-8')

            # Should NOT contain unescaped HTML tag
            assert '<img src=x onerror=alert(1)>' not in html

            # Should contain escaped version
            assert '&lt;img' in html


class TestMediaLogging:
    """Test media upload logging (v1.4.1)"""

    def test_save_media_logs_success(self, app, caplog):
        """Test successful upload logs at INFO level"""
        import logging

        image_data = create_test_image(800, 600, 'PNG')

        with app.app_context():
            with caplog.at_level(logging.INFO):
                media_info = save_media(image_data, 'test.png')

            # Check success log exists
            assert "Media upload successful" in caplog.text
            assert 'filename="test.png"' in caplog.text
            assert f'stored="{media_info["stored_filename"]}"' in caplog.text
            assert f'size={media_info["size"]}b' in caplog.text
            # optimized flag should be present
            assert 'optimized=' in caplog.text
            # variants count should be present
            assert 'variants=' in caplog.text

    def test_save_media_logs_validation_failure(self, app, caplog):
        """Test validation failure logs at WARNING level"""
        import logging

        # Create invalid data (corrupted image)
        invalid_data = b'not an image'

        with app.app_context():
            with caplog.at_level(logging.WARNING):
                with pytest.raises(ValueError):
                    save_media(invalid_data, 'corrupt.jpg')

            # Check validation failure log
            assert "Media upload validation failed" in caplog.text
            assert 'filename="corrupt.jpg"' in caplog.text
            assert f'size={len(invalid_data)}b' in caplog.text
            assert 'error=' in caplog.text

    def test_save_media_logs_optimization_failure(self, app, caplog, monkeypatch):
        """Test optimization failure logs at WARNING level"""
        import logging
        from starpunk import media

        # Mock optimize_image to raise ValueError
        def mock_optimize_image(image_data, original_size=None):
            raise ValueError("Image cannot be optimized to target size. Please use a smaller or lower-resolution image.")

        monkeypatch.setattr(media, 'optimize_image', mock_optimize_image)

        image_data = create_test_image(800, 600, 'PNG')

        with app.app_context():
            with caplog.at_level(logging.WARNING):
                with pytest.raises(ValueError):
                    save_media(image_data, 'test.png')

            # Check optimization failure log
            assert "Media upload optimization failed" in caplog.text
            assert 'filename="test.png"' in caplog.text
            assert f'size={len(image_data)}b' in caplog.text
            assert 'error=' in caplog.text

    def test_save_media_logs_variant_failure(self, app, caplog, monkeypatch):
        """Test variant generation failure causes atomic rollback (v1.5.0 Phase 4)"""
        import logging
        from starpunk import media

        # Mock generate_all_variants to raise an exception
        def mock_generate_all_variants(*args, **kwargs):
            raise RuntimeError("Variant generation failed")

        monkeypatch.setattr(media, 'generate_all_variants', mock_generate_all_variants)

        image_data = create_test_image(800, 600, 'PNG')

        with app.app_context():
            with caplog.at_level(logging.WARNING):
                # Should fail due to atomic operation (v1.5.0 Phase 4)
                with pytest.raises(RuntimeError, match="Variant generation failed"):
                    save_media(image_data, 'test.png')

            # Check atomic operation failure log
            assert "Media upload atomic operation failed" in caplog.text
            assert 'filename="test.png"' in caplog.text
            assert 'error="Variant generation failed"' in caplog.text

    def test_save_media_logs_unexpected_error(self, app, caplog, monkeypatch):
        """Test unexpected error logs at ERROR level"""
        import logging
        from starpunk import media
        from pathlib import Path as OriginalPath

        # Mock Path.write_bytes to raise OSError (simulating disk full)
        def mock_write_bytes(self, data):
            raise OSError("[Errno 28] No space left on device")

        monkeypatch.setattr(Path, 'write_bytes', mock_write_bytes)

        image_data = create_test_image(800, 600, 'PNG')

        with app.app_context():
            with caplog.at_level(logging.ERROR):
                with pytest.raises(OSError):
                    save_media(image_data, 'test.png')

            # Check unexpected error log
            assert "Media upload failed unexpectedly" in caplog.text
            assert 'filename="test.png"' in caplog.text
            assert 'error_type="OSError"' in caplog.text
            assert 'error=' in caplog.text


@pytest.fixture
def sample_note(app):
    """Create a sample note for testing"""
    from starpunk.notes import create_note

    with app.app_context():
        note = create_note("Test note content", published=True)
        yield note


class TestAtomicVariantGeneration:
    """
    Test atomic variant generation (v1.5.0 Phase 4)

    Tests that variant generation is atomic with database commits,
    preventing orphaned files or database records.
    """

    def test_atomic_media_save_success(self, app):
        """Test that media save operation is fully atomic on success"""
        from pathlib import Path
        from starpunk.media import save_media

        # Create test image
        img_data = create_test_image(1600, 1200, 'JPEG')

        with app.app_context():
            # Save media
            result = save_media(img_data, 'test_atomic.jpg')

            # Verify media record was created
            assert result['id'] > 0
            assert result['filename'] == 'test_atomic.jpg'

            # Verify original file exists in final location
            media_dir = Path(app.config['DATA_PATH']) / 'media'
            original_path = media_dir / result['path']
            assert original_path.exists(), "Original file should exist in final location"

            # Verify variant files exist in final location
            for variant in result['variants']:
                variant_path = media_dir / variant['path']
                assert variant_path.exists(), f"Variant {variant['variant_type']} should exist"

            # Verify no temp files left behind
            temp_dir = media_dir / '.tmp'
            if temp_dir.exists():
                temp_files = list(temp_dir.glob('**/*'))
                temp_files = [f for f in temp_files if f.is_file()]
                assert len(temp_files) == 0, "No temp files should remain after successful save"

    def test_file_move_failure_rolls_back_database(self, app, monkeypatch):
        """Test that file move failure rolls back database transaction"""
        from pathlib import Path
        from starpunk.media import save_media
        import shutil

        # Create test image
        img_data = create_test_image(1600, 1200, 'JPEG')

        with app.app_context():
            from starpunk.database import get_db

            # Mock shutil.move to fail
            original_move = shutil.move
            call_count = [0]

            def mock_move(src, dst):
                call_count[0] += 1
                # Fail on first move (original file)
                if call_count[0] == 1:
                    raise OSError("File move failed")
                return original_move(src, dst)

            monkeypatch.setattr(shutil, 'move', mock_move)

            # Count media records before operation
            db = get_db(app)
            media_count_before = db.execute("SELECT COUNT(*) FROM media").fetchone()[0]

            # Try to save media - should fail
            with pytest.raises(OSError, match="File move failed"):
                save_media(img_data, 'test_rollback.jpg')

            # Verify no new media records were added (transaction rolled back)
            media_count_after = db.execute("SELECT COUNT(*) FROM media").fetchone()[0]
            assert media_count_after == media_count_before, "No media records should be added on failure"

            # Verify temp files were cleaned up
            media_dir = Path(app.config['DATA_PATH']) / 'media'
            temp_dir = media_dir / '.tmp'
            if temp_dir.exists():
                temp_files = list(temp_dir.glob('**/*'))
                temp_files = [f for f in temp_files if f.is_file()]
                assert len(temp_files) == 0, "Temp files should be cleaned up after file move failure"

            # Restore original move
            monkeypatch.setattr(shutil, 'move', original_move)

    def test_startup_recovery_cleans_orphaned_temp_files(self, app):
        """Test that startup recovery detects and cleans orphaned temp files"""
        from pathlib import Path
        from starpunk.media import cleanup_orphaned_temp_files
        import logging

        with app.app_context():
            media_dir = Path(app.config['DATA_PATH']) / 'media'
            temp_dir = media_dir / '.tmp'
            temp_dir.mkdir(parents=True, exist_ok=True)

            # Create orphaned temp subdirectory with files
            orphan_dir = temp_dir / 'orphaned_test_12345678'
            orphan_dir.mkdir(parents=True, exist_ok=True)

            # Create some fake orphaned files
            orphan_file1 = orphan_dir / 'test_thumb.jpg'
            orphan_file2 = orphan_dir / 'test_small.jpg'
            orphan_file1.write_bytes(b'fake image data')
            orphan_file2.write_bytes(b'fake image data')

            # Run cleanup
            with app.test_request_context():
                cleanup_orphaned_temp_files(app)

            # Verify files were cleaned up
            assert not orphan_file1.exists(), "Orphaned file 1 should be deleted"
            assert not orphan_file2.exists(), "Orphaned file 2 should be deleted"
            assert not orphan_dir.exists(), "Orphaned directory should be deleted"

    def test_startup_recovery_logs_orphaned_files(self, app, caplog):
        """Test that startup recovery logs warnings for orphaned files"""
        from pathlib import Path
        from starpunk.media import cleanup_orphaned_temp_files
        import logging

        with app.app_context():
            media_dir = Path(app.config['DATA_PATH']) / 'media'
            temp_dir = media_dir / '.tmp'
            temp_dir.mkdir(parents=True, exist_ok=True)

            # Create orphaned temp subdirectory with files
            orphan_dir = temp_dir / 'orphaned_test_99999999'
            orphan_dir.mkdir(parents=True, exist_ok=True)

            # Create some fake orphaned files
            orphan_file = orphan_dir / 'test_medium.jpg'
            orphan_file.write_bytes(b'fake image data')

            # Run cleanup with logging
            with caplog.at_level(logging.WARNING):
                cleanup_orphaned_temp_files(app)

            # Verify warning was logged
            assert "Found orphaned temp directory from failed operation" in caplog.text
            assert "orphaned_test_99999999" in caplog.text

