"""
Unit tests for PowerPointTranslator.

Tests cover:
- Text box/run translation (AC1)
- Slide layout preservation (AC2)
- Image preservation (AC3)
- Animation preservation (AC4)
- PowerPoint compatibility (AC5)
- Error handling (AC6)
- Provider integration (AC7)
- Progress callback
"""

import pytest
import tempfile
import zipfile
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch
from typing import List

from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.shapes import MSO_SHAPE_TYPE, MSO_SHAPE

from translators.pptx_translator import (
    PowerPointTranslator,
    PptxProcessorError,
    pptx_translator,
)
from services.providers.schemas import TranslationRequest, TranslationResponse


class MockTranslationProvider:
    """Mock translation provider for testing."""

    def __init__(self, translations: dict = None):
        self._translations = translations or {}
        self._call_count = 0
        self._requests_received: List[TranslationRequest] = []

    def get_name(self) -> str:
        return "mock"

    def is_available(self) -> bool:
        return True

    def translate_text(self, request: TranslationRequest) -> TranslationResponse:
        self._call_count += 1
        self._requests_received.append(request)

        text = request.text
        translated = self._translations.get(text, f"TR_{text}")

        return TranslationResponse(
            translated_text=translated,
            provider_name="mock",
            source_language=request.source_language,
        )

    def translate_batch(
        self, requests: List[TranslationRequest]
    ) -> List[TranslationResponse]:
        return [self.translate_text(req) for req in requests]


class TestPptxProcessorError:
    """Tests for PptxProcessorError exception."""

    def test_error_with_default_message(self):
        """Test error with default message from code."""
        error = PptxProcessorError(PptxProcessorError.INVALID_FORMAT)
        assert error.code == "INVALID_FORMAT"
        assert "pptx" in error.message.lower()
        assert error.to_dict()["error"] == "INVALID_FORMAT"

    def test_error_with_custom_message(self):
        """Test error with custom message."""
        error = PptxProcessorError(
            PptxProcessorError.PPTX_CORRUPTED, message="Custom error message"
        )
        assert error.message == "Custom error message"

    def test_error_with_details(self):
        """Test error with details dict."""
        error = PptxProcessorError(
            PptxProcessorError.PPTX_TOO_LARGE, details={"size_mb": 100, "max_mb": 50}
        )
        assert error.details["size_mb"] == 100
        assert error.to_dict()["details"]["size_mb"] == 100

    def test_all_error_codes_have_messages(self):
        """Test all error codes have default messages."""
        codes = [
            PptxProcessorError.INVALID_FORMAT,
            PptxProcessorError.PPTX_CORRUPTED,
            PptxProcessorError.PPTX_READ_ERROR,
            PptxProcessorError.PPTX_WRITE_ERROR,
            PptxProcessorError.PPTX_TOO_LARGE,
        ]
        for code in codes:
            error = PptxProcessorError(code)
            assert error.message
            assert len(error.message) > 0


class TestPowerPointTranslatorInit:
    """Tests for PowerPointTranslator initialization."""

    def test_init_without_provider(self):
        """Test initialization without provider (uses legacy fallback)."""
        translator = PowerPointTranslator()
        assert translator._provider is None

    def test_init_with_provider(self):
        """Test initialization with provider."""
        mock_provider = MockTranslationProvider()
        translator = PowerPointTranslator(provider=mock_provider)
        assert translator._provider is mock_provider

    def test_set_provider(self):
        """Test setting provider after initialization."""
        translator = PowerPointTranslator()
        mock_provider = MockTranslationProvider()
        translator.set_provider(mock_provider)
        assert translator._provider is mock_provider

    def test_set_custom_prompt(self):
        """Test setting custom prompt."""
        translator = PowerPointTranslator()
        translator.set_custom_prompt("Translate to French")
        assert translator._custom_prompt == "Translate to French"


class TestFileValidation:
    """Tests for file validation."""

    def test_validate_nonexistent_file(self):
        """Test validation of non-existent file."""
        translator = PowerPointTranslator()
        with pytest.raises(PptxProcessorError) as exc_info:
            translator._validate_file(Path("/nonexistent/file.pptx"))
        assert exc_info.value.code == PptxProcessorError.PPTX_READ_ERROR

    def test_validate_wrong_extension(self, tmp_path):
        """Test validation of file with wrong extension."""
        translator = PowerPointTranslator()
        wrong_file = tmp_path / "test.txt"
        wrong_file.write_text("not a pptx file")

        with pytest.raises(PptxProcessorError) as exc_info:
            translator._validate_file(wrong_file)
        assert exc_info.value.code == PptxProcessorError.INVALID_FORMAT

    def test_validate_invalid_magic_bytes(self, tmp_path):
        """Test validation of file with invalid magic bytes."""
        translator = PowerPointTranslator()
        invalid_file = tmp_path / "test.pptx"
        invalid_file.write_bytes(b"Not a ZIP file")

        with pytest.raises(PptxProcessorError) as exc_info:
            translator._validate_file(invalid_file)
        assert exc_info.value.code == PptxProcessorError.INVALID_FORMAT

    def test_validate_file_too_large(self, tmp_path):
        """Test validation of file exceeding size limit."""
        translator = PowerPointTranslator()
        translator.MAX_FILE_SIZE_MB = 0.001  # Set very low limit for testing

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])
        large_file = tmp_path / "large.pptx"
        prs.save(str(large_file))

        with pytest.raises(PptxProcessorError) as exc_info:
            translator._validate_file(large_file)
        assert exc_info.value.code == PptxProcessorError.PPTX_TOO_LARGE

        translator.MAX_FILE_SIZE_MB = 50  # Reset

    def test_validate_valid_file(self, tmp_path):
        """Test validation of valid file."""
        translator = PowerPointTranslator()

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])
        valid_file = tmp_path / "valid.pptx"
        prs.save(str(valid_file))

        translator._validate_file(valid_file)


class TestTextBoxTranslation:
    """Tests for text box/run translation (AC1)."""

    def test_translate_text_boxes(self, tmp_path):
        """Test that text boxes are translated."""
        mock_provider = MockTranslationProvider(
            {
                "Hello World": "Bonjour Monde",
            }
        )
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tf = textbox.text_frame
        p = tf.paragraphs[0]
        p.text = "Hello World"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        prs_out = Presentation(str(output_file))
        slide_out = prs_out.slides[0]

        text_found = False
        for shape in slide_out.shapes:
            if shape.has_text_frame:
                for para in shape.text_frame.paragraphs:
                    for run in para.runs:
                        if "Bonjour" in run.text:
                            text_found = True

        assert text_found, "Translated text not found in output"

    def test_multiple_runs_in_paragraph(self, tmp_path):
        """Test that adjacent same-style runs in a paragraph are merged
        into ONE translation unit (whole-sentence translation)."""
        mock_provider = MockTranslationProvider(
            {
                "Hello World": "Bonjour Monde",
            }
        )
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tf = textbox.text_frame
        p = tf.paragraphs[0]
        run1 = p.add_run()
        run1.text = "Hello"
        run2 = p.add_run()
        run2.text = " World"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        # The two same-style runs were sent as ONE merged unit
        sent = [req.text for req in mock_provider._requests_received]
        assert "Hello World" in sent
        assert "Hello" not in sent and "World" not in sent

        prs_out = Presentation(str(output_file))
        slide_out = prs_out.slides[0]

        para_out = None
        for shape in slide_out.shapes:
            if shape.has_text_frame and shape.text_frame.paragraphs[0].runs:
                para_out = shape.text_frame.paragraphs[0]
                break
        assert para_out is not None
        assert para_out.runs[0].text == "Bonjour Monde"
        assert para_out.runs[1].text == ""


class TestTableTranslation:
    """Tests for table translation (AC1)."""

    def test_translate_table_cells(self, tmp_path):
        """Test that table cells are translated."""
        mock_provider = MockTranslationProvider(
            {
                "Header": "En-tete",
                "Data": "Donnees",
            }
        )
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        rows, cols = 2, 2
        table = slide.shapes.add_table(
            rows, cols, Inches(1), Inches(1), Inches(4), Inches(2)
        ).table

        table.cell(0, 0).text = "Header"
        table.cell(0, 1).text = "Header"
        table.cell(1, 0).text = "Data"
        table.cell(1, 1).text = "Data"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        prs_out = Presentation(str(output_file))
        slide_out = prs_out.slides[0]

        for shape in slide_out.shapes:
            if shape.has_table:
                assert (
                    "En-tete" in shape.table.cell(0, 0).text
                    or shape.table.cell(0, 1).text
                )


class TestGroupShapeHandling:
    """Tests for group shape handling."""

    def test_group_shapes_translated(self, tmp_path):
        """Test that grouped shapes are translated."""
        mock_provider = MockTranslationProvider(
            {
                "Grouped Text": "Texte Groupe",
            }
        )
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        shape1 = slide.shapes.add_shape(
            MSO_SHAPE.RECTANGLE, Inches(1), Inches(1), Inches(2), Inches(1)
        )
        shape2 = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(2), Inches(1))
        tf = shape2.text_frame
        p = tf.paragraphs[0]
        p.text = "Grouped Text"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        prs_out = Presentation(str(output_file))
        assert len(prs_out.slides) == 1


class TestImagePreservation:
    """Tests for image preservation (AC3)."""

    def test_images_preserved(self, tmp_path):
        """Test that images remain in their original positions."""
        mock_provider = MockTranslationProvider({"Test": "TestFR"})
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        # Add a textbox with text to ensure translation happens
        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tf = textbox.text_frame
        p = tf.paragraphs[0]
        p.text = "Test"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        prs_out = Presentation(str(output_file))
        assert len(prs_out.slides) == 1

        # Verify the slide was processed (text was translated)
        text_found = False
        for shape in prs_out.slides[0].shapes:
            if shape.has_text_frame:
                for para in shape.text_frame.paragraphs:
                    if "TestFR" in para.text or "TR_Test" in para.text:
                        text_found = True
        assert text_found, "Translation should have occurred"

    def test_shape_count_preserved(self, tmp_path):
        """Test that the number of shapes is preserved after translation."""
        mock_provider = MockTranslationProvider({"Hello": "Bonjour"})
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        # Add multiple shapes
        textbox1 = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(2), Inches(1))
        textbox1.text_frame.paragraphs[0].text = "Hello"
        textbox2 = slide.shapes.add_textbox(Inches(3), Inches(1), Inches(2), Inches(1))
        textbox2.text_frame.paragraphs[0].text = "Hello"

        original_shape_count = len(slide.shapes)

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        prs_out = Presentation(str(output_file))
        assert len(prs_out.slides[0].shapes) == original_shape_count


class TestAnimationPreservation:
    """Tests for animation preservation (AC4)."""

    def test_animations_preserved(self, tmp_path):
        """Test that animations are preserved (python-pptx handles automatically)."""
        mock_provider = MockTranslationProvider({"Test": "TestFR"})
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tf = textbox.text_frame
        p = tf.paragraphs[0]
        p.text = "Test"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        prs_out = Presentation(str(output_file))
        assert len(prs_out.slides) == 1


class TestNotesSlideHandling:
    """Tests for notes slide handling."""

    def test_notes_translated(self, tmp_path):
        """Test that speaker notes are translated."""
        mock_provider = MockTranslationProvider(
            {
                "Speaker notes": "Notes du presentateur",
            }
        )
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        notes_slide = slide.notes_slide
        notes_tf = notes_slide.notes_text_frame
        notes_tf.text = "Speaker notes"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        prs_out = Presentation(str(output_file))
        slide_out = prs_out.slides[0]

        if slide_out.has_notes_slide:
            notes_out = slide_out.notes_slide.notes_text_frame.text
            assert "presentateur" in notes_out or "TR_" in notes_out


class TestProgressCallback:
    """Tests for progress callback."""

    def test_progress_callback_called(self, tmp_path):
        """Test that progress callback is called."""
        mock_provider = MockTranslationProvider({"Test": "Test FR"})
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tf = textbox.text_frame
        p = tf.paragraphs[0]
        p.text = "Test"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        progress_events = []

        def callback(event):
            progress_events.append(event)

        translator.translate_file(
            input_file, output_file, "fr", progress_callback=callback
        )

        assert len(progress_events) >= 1
        assert "slide" in progress_events[0]
        assert "total_slides" in progress_events[0]
        assert "runs_translated" in progress_events[0]

    def test_progress_callback_without_callback(self, tmp_path):
        """Test that translation works without callback."""
        mock_provider = MockTranslationProvider({"Test": "Test FR"})
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tf = textbox.text_frame
        p = tf.paragraphs[0]
        p.text = "Test"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        result = translator.translate_file(input_file, output_file, "fr")
        assert result == output_file


class TestProviderIntegration:
    """Tests for provider integration (AC7)."""

    def test_provider_receives_correct_requests(self, tmp_path):
        """Test that provider receives correctly formatted requests."""
        mock_provider = MockTranslationProvider({"Hello": "Bonjour"})
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tf = textbox.text_frame
        p = tf.paragraphs[0]
        p.text = "Hello"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr", source_language="en")

        assert len(mock_provider._requests_received) >= 1
        req = mock_provider._requests_received[0]
        assert req.text == "Hello"
        assert req.target_language == "fr"
        assert req.source_language == "en"

    def test_custom_prompt_passed_to_provider(self, tmp_path):
        """Test that custom prompt is passed via metadata."""
        mock_provider = MockTranslationProvider({"Hello": "Bonjour"})
        translator = PowerPointTranslator(provider=mock_provider)
        translator.set_custom_prompt("Translate to formal French")

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tf = textbox.text_frame
        p = tf.paragraphs[0]
        p.text = "Hello"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        req = mock_provider._requests_received[0]
        assert req.metadata is not None
        assert req.metadata.get("custom_prompt") == "Translate to formal French"


class TestErrorHandling:
    """Tests for error handling (AC6)."""

    def test_corrupted_file_error(self, tmp_path):
        """Test that corrupted file raises PptxProcessorError."""
        translator = PowerPointTranslator()

        corrupted_file = tmp_path / "corrupted.pptx"
        corrupted_file.write_bytes(b"PK\x03\x04" + b"\x00" * 100)

        with pytest.raises(PptxProcessorError) as exc_info:
            translator.translate_file(corrupted_file, tmp_path / "out.pptx", "fr")

        assert exc_info.value.code in [
            PptxProcessorError.PPTX_CORRUPTED,
            PptxProcessorError.PPTX_READ_ERROR,
        ]

    def test_invalid_format_error_details(self, tmp_path):
        """Test that invalid format error includes details."""
        translator = PowerPointTranslator()

        invalid_file = tmp_path / "test.txt"
        invalid_file.write_text("not pptx")

        with pytest.raises(PptxProcessorError) as exc_info:
            translator._validate_file(invalid_file)

        error = exc_info.value
        assert error.to_dict()["error"] == "INVALID_FORMAT"
        assert "details" in error.to_dict()


class TestLegacyFallback:
    """Tests for legacy translation_service fallback."""

    def test_fallback_to_legacy_service(self, tmp_path):
        """Test that legacy service is used when no provider set."""
        translator = PowerPointTranslator()

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tf = textbox.text_frame
        p = tf.paragraphs[0]
        p.text = "Hello"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        with patch("services.translation_service.translation_service") as mock_service:
            mock_service.translate_batch.return_value = ["Bonjour"]

            translator.translate_file(input_file, output_file, "fr")

            mock_service.translate_batch.assert_called_once()

    def test_global_instance_exists(self):
        """Test that global pptx_translator instance exists."""
        from translators import pptx_translator

        assert pptx_translator is not None
        assert isinstance(pptx_translator, PowerPointTranslator)


class TestPowerPointCompatibility:
    """Tests for PowerPoint compatibility (AC5)."""

    def test_valid_pptx_structure(self, tmp_path):
        """Test that output file has valid pptx structure that PowerPoint can open."""
        mock_provider = MockTranslationProvider({"Test": "TestFR"})
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[0])

        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tf = textbox.text_frame
        p = tf.paragraphs[0]
        p.text = "Test"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        assert zipfile.is_zipfile(output_file), "Output is not a valid pptx (ZIP) file"

        prs_out = Presentation(str(output_file))
        assert prs_out is not None

        with zipfile.ZipFile(output_file, "r") as zf:
            files = zf.namelist()
            assert "[Content_Types].xml" in files, "Missing Content_Types.xml"
            assert any("ppt/presentation.xml" in f for f in files), (
                "Missing presentation.xml"
            )

    def test_complex_presentation_compatibility(self, tmp_path):
        """Test compatibility with complex presentations containing multiple slides and shapes."""
        mock_provider = MockTranslationProvider(
            {
                "Title": "Titre",
                "Subtitle": "Sous-titre",
                "Content": "Contenu",
            }
        )
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()

        slide1 = prs.slides.add_slide(prs.slide_layouts[0])
        title1 = slide1.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1))
        tf1 = title1.text_frame
        p1 = tf1.paragraphs[0]
        p1.text = "Title"

        slide2 = prs.slides.add_slide(prs.slide_layouts[0])
        title2 = slide2.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1))
        tf2 = title2.text_frame
        p2 = tf2.paragraphs[0]
        p2.text = "Subtitle"

        content = slide2.shapes.add_textbox(Inches(1), Inches(2), Inches(8), Inches(4))
        tf3 = content.text_frame
        p3 = tf3.paragraphs[0]
        p3.text = "Content"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        prs_out = Presentation(str(output_file))
        assert len(prs_out.slides) == 2


class TestMultipleSlides:
    """Tests for multi-slide presentations."""

    def test_multiple_slides_translated(self, tmp_path):
        """Test that all slides are translated."""
        mock_provider = MockTranslationProvider(
            {
                "Slide1Text": "Diapo1Texte",
                "Slide2Text": "Diapo2Texte",
            }
        )
        translator = PowerPointTranslator(provider=mock_provider)

        prs = Presentation()

        slide1 = prs.slides.add_slide(prs.slide_layouts[0])
        tb1 = slide1.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tb1.text_frame.paragraphs[0].text = "Slide1Text"

        slide2 = prs.slides.add_slide(prs.slide_layouts[0])
        tb2 = slide2.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
        tb2.text_frame.paragraphs[0].text = "Slide2Text"

        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))

        translator.translate_file(input_file, output_file, "fr")

        prs_out = Presentation(str(output_file))
        assert len(prs_out.slides) == 2


class TestPptxProcessorErrorHTTPMapping:
    """Tests for PptxProcessorError HTTP status code mapping (AC6)."""

    def test_invalid_format_returns_400_status(self):
        """Test that INVALID_FORMAT error maps to HTTP 400."""
        error = PptxProcessorError(PptxProcessorError.INVALID_FORMAT)
        error_dict = error.to_dict()
        
        assert error_dict["error"] == "INVALID_FORMAT"
        assert "pptx" in error_dict["message"].lower()
        # HTTP mapping: 400 for INVALID_FORMAT

    def test_pptx_too_large_returns_413_status(self):
        """Test that PPTX_TOO_LARGE error maps to HTTP 413."""
        error = PptxProcessorError(
            PptxProcessorError.PPTX_TOO_LARGE,
            details={"size_mb": 100, "max_mb": 50}
        )
        error_dict = error.to_dict()
        
        assert error_dict["error"] == "PPTX_TOO_LARGE"
        assert "volumineux" in error_dict["message"].lower()
        # HTTP mapping: 413 for PPTX_TOO_LARGE

    def test_pptx_write_error_returns_500_status(self):
        """Test that PPTX_WRITE_ERROR error maps to HTTP 500."""
        error = PptxProcessorError(PptxProcessorError.PPTX_WRITE_ERROR)
        error_dict = error.to_dict()
        
        assert error_dict["error"] == "PPTX_WRITE_ERROR"
        # HTTP mapping: 500 for PPTX_WRITE_ERROR

    def test_all_error_codes_return_structured_json(self):
        """Test that all error codes return properly structured JSON."""
        codes = [
            PptxProcessorError.INVALID_FORMAT,
            PptxProcessorError.PPTX_CORRUPTED,
            PptxProcessorError.PPTX_READ_ERROR,
            PptxProcessorError.PPTX_WRITE_ERROR,
            PptxProcessorError.PPTX_TOO_LARGE,
        ]

        for code in codes:
            error = PptxProcessorError(code)
            error_dict = error.to_dict()

            # All errors must have these fields
            assert "error" in error_dict, f"Missing 'error' field for {code}"
            assert "message" in error_dict, f"Missing 'message' field for {code}"
            assert error_dict["error"] == code
            assert isinstance(error_dict["message"], str)
            assert len(error_dict["message"]) > 0


# Espace de noms DrawingML pour construire des éléments XML dans les tests
_A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"


class TestRunMerging:
    """Fusion des runs adjacents de même style en une seule unité.

    Une phrase coupée en plusieurs runs de même style doit partir au
    provider en UN morceau ; la traduction revient dans le premier run
    et les autres sont vidés. Approche identique au traducteur Word.
    """

    def _build_pptx(self, tmp_path, populate):
        """Crée un .pptx avec une zone de texte remplie par populate(paragraph)."""
        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[6])  # page vierge
        textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(6), Inches(1))
        populate(textbox.text_frame.paragraphs[0])
        input_file = tmp_path / "input.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(str(input_file))
        return input_file, output_file

    def _first_text_paragraph(self, path):
        """Relit le fichier traduit et renvoie le premier paragraphe non vide."""
        prs_out = Presentation(str(path))
        for shape in prs_out.slides[0].shapes:
            if shape.has_text_frame and shape.text_frame.text.strip():
                return shape.text_frame.paragraphs[0]
        raise AssertionError("aucun paragraphe de texte trouvé dans la sortie")

    def test_three_same_style_runs_merged_into_one_unit(self, tmp_path):
        """3 runs de même style formant une phrase → UNE seule unité envoyée,
        la traduction dans le premier run, les suivants vidés."""
        provider = MockTranslationProvider(
            {"This is a very nice day.": "C'est un tres beau jour."}
        )
        translator = PowerPointTranslator(provider=provider)

        def populate(p):
            for chunk in ("This is ", "a very ", "nice day."):
                run = p.add_run()
                run.text = chunk

        input_file, output_file = self._build_pptx(tmp_path, populate)
        translator.translate_file(input_file, output_file, "fr")

        sent = [req.text for req in provider._requests_received]
        assert sent == ["This is a very nice day."], (
            f"attendu une seule unité fusionnée, reçu: {sent}"
        )

        para = self._first_text_paragraph(output_file)
        assert len(para.runs) == 3, "les éléments a:r ne doivent pas être supprimés"
        assert para.runs[0].text == "C'est un tres beau jour."
        assert para.runs[1].text == ""
        assert para.runs[2].text == ""

    def test_two_styles_stay_separate_units(self, tmp_path):
        """Runs gras + normal (signatures différentes) → 2 unités distinctes,
        chaque traduction reste dans son run d'origine."""
        provider = MockTranslationProvider({"Hello": "Bonjour", "World": "Monde"})
        translator = PowerPointTranslator(provider=provider)

        def populate(p):
            bold = p.add_run()
            bold.text = "Hello "
            bold.font.bold = True
            normal = p.add_run()
            normal.text = "World"

        input_file, output_file = self._build_pptx(tmp_path, populate)
        translator.translate_file(input_file, output_file, "fr")

        sent = [req.text for req in provider._requests_received]
        assert sorted(sent) == ["Hello", "World"], (
            f"attendu 2 unités distinctes, reçu: {sent}"
        )

        para = self._first_text_paragraph(output_file)
        # chaque traduction dans son run d'origine, styles intacts
        assert para.runs[0].text == "Bonjour "  # espace de fin conservé
        assert para.runs[0].font.bold is True
        assert para.runs[1].text == "Monde"
        assert para.runs[1].font.bold is None

    def test_line_break_prevents_merging(self, tmp_path):
        """Deux runs de même style séparés par un a:br ne sont JAMAIS fusionnés."""
        provider = MockTranslationProvider(
            {"First line": "Premiere ligne", "Second line": "Seconde ligne"}
        )
        translator = PowerPointTranslator(provider=provider)

        def populate(p):
            r1 = p.add_run()
            r1.text = "First line"
            r2 = p.add_run()
            r2.text = "Second line"
            # saut de ligne DrawingML entre les deux runs
            br = p._p.makeelement(f"{{{_A_NS}}}br", {})
            r2._r.addprevious(br)

        input_file, output_file = self._build_pptx(tmp_path, populate)
        translator.translate_file(input_file, output_file, "fr")

        sent = [req.text for req in provider._requests_received]
        assert sorted(sent) == ["First line", "Second line"], (
            f"les runs séparés par un a:br ne doivent pas être fusionnés: {sent}"
        )

        para = self._first_text_paragraph(output_file)
        assert para.runs[0].text == "Premiere ligne"
        assert para.runs[1].text == "Seconde ligne"
        # le a:br est toujours là, entre les deux runs
        child_tags = [child.tag.split("}")[-1] for child in para._p]
        assert child_tags == ["r", "br", "r"], (
            f"structure du paragraphe modifiée: {child_tags}"
        )

    def test_group_edge_whitespace_preserved(self, tmp_path):
        """Les espaces de début/fin du groupe sont conservés (lead/trail)."""
        provider = MockTranslationProvider({"Leading text": "Texte d'introduction"})
        translator = PowerPointTranslator(provider=provider)

        def populate(p):
            r1 = p.add_run()
            r1.text = "  Leading"
            r2 = p.add_run()
            r2.text = " text  "

        input_file, output_file = self._build_pptx(tmp_path, populate)
        translator.translate_file(input_file, output_file, "fr")

        # le provider reçoit le texte nu, sans les espaces de bord
        sent = [req.text for req in provider._requests_received]
        assert sent == ["Leading text"]

        para = self._first_text_paragraph(output_file)
        assert para.runs[0].text == "  Texte d'introduction  "
        assert para.runs[1].text == ""

    def test_newline_in_translation_survives_write(self, tmp_path):
        """Un saut de ligne dans la traduction ne casse pas l'écriture :
        \n est un caractère XML valide, l'accesseur text de a:r
        (CT_RegularTextRun) ne l'échappe pas — il atterrit tel quel dans
        le <a:t> et ressort intact à la relecture."""
        provider = MockTranslationProvider({"One line": "Ligne un\nLigne deux"})
        translator = PowerPointTranslator(provider=provider)

        def populate(p):
            run = p.add_run()
            run.text = "One line"

        input_file, output_file = self._build_pptx(tmp_path, populate)

        # ne doit pas lever d'exception
        translator.translate_file(input_file, output_file, "fr")

        para = self._first_text_paragraph(output_file)
        assert "Ligne un" in para.runs[0].text
        assert "Ligne deux" in para.runs[0].text
        assert "\n" in para.runs[0].text


class TestPptxTableRtlAndAutoFit:
    """Tests for table RTL and text-frame word wrap / auto-fit."""

    def test_table_rtl_attribute_applied(self, tmp_path):
        """Table should receive <a:tblPr rtl='1'> for RTL target."""
        translator = PowerPointTranslator(
            provider=MockTranslationProvider({"Item": "مورد"})
        )

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[6])
        shape = slide.shapes.add_table(1, 1, 100, 100, 200, 100)
        shape.table.cell(0, 0).text = "Item"

        input_file = tmp_path / "input_tbl.pptx"
        output_file = tmp_path / "output_tbl.pptx"
        prs.save(input_file)

        translator.translate_file(input_file, output_file, "fa")

        prs_out = Presentation(output_file)
        tbl_out = prs_out.slides[0].shapes[0]
        tbl_el = tbl_out._element.find(".//{http://schemas.openxmlformats.org/drawingml/2006/main}tbl")
        tblPr = tbl_el.find("{http://schemas.openxmlformats.org/drawingml/2006/main}tblPr")
        assert tblPr is not None
        assert tblPr.get("rtl") == "1"

    def test_word_wrap_and_autofit_configured(self, tmp_path):
        """Text frames should have word_wrap and auto_size enabled."""
        translator = PowerPointTranslator(
            provider=MockTranslationProvider({"Hello": "Bonjour"})
        )

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[6])
        shape = slide.shapes.add_textbox(100, 100, 200, 50)
        shape.text_frame.text = "Hello"
        shape.text_frame.word_wrap = False

        input_file = tmp_path / "input_wrap.pptx"
        output_file = tmp_path / "output_wrap.pptx"
        prs.save(input_file)

        translator.translate_file(input_file, output_file, "fr")

        prs_out = Presentation(output_file)
        tf_out = prs_out.slides[0].shapes[0].text_frame
        assert tf_out.word_wrap is True
