"""
Unit tests for WordTranslator.

Tests cover:
- Run-level text translation (preserving formatting)
- Table cell translation (including nested tables)
- Header/footer translation
- Error handling (corrupted, invalid format, too large)
- Progress callback
- Provider integration
"""

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

from docx import Document
from docx.shared import Pt, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH

from translators.word_translator import (
    WordTranslator,
    WordProcessorError,
    word_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 TestWordProcessorError:
    """Tests for WordProcessorError exception."""

    def test_error_with_default_message(self):
        """Test error with default message from code."""
        error = WordProcessorError(WordProcessorError.INVALID_FORMAT)
        assert error.code == "INVALID_FORMAT"
        assert "docx" in error.message.lower() or "format" in error.message.lower()
        assert error.to_dict()["error"] == "INVALID_FORMAT"

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

    def test_error_with_details(self):
        """Test error with details dict."""
        error = WordProcessorError(
            WordProcessorError.DOCX_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 = [
            WordProcessorError.INVALID_FORMAT,
            WordProcessorError.DOCX_CORRUPTED,
            WordProcessorError.DOCX_READ_ERROR,
            WordProcessorError.DOCX_WRITE_ERROR,
            WordProcessorError.DOCX_TOO_LARGE,
        ]
        for code in codes:
            error = WordProcessorError(code)
            assert error.message
            assert len(error.message) > 0


class TestWordTranslatorInit:
    """Tests for WordTranslator initialization."""

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

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

    def test_set_provider(self):
        """Test setting provider after initialization."""
        translator = WordTranslator()
        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 = WordTranslator()
        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 = WordTranslator()
        with pytest.raises(WordProcessorError) as exc_info:
            translator._validate_file(Path("/nonexistent/file.docx"))
        assert exc_info.value.code == WordProcessorError.DOCX_READ_ERROR

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

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

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

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

    def test_validate_file_too_large(self, tmp_path):
        """Test validation of file exceeding size limit."""
        translator = WordTranslator()
        translator.MAX_FILE_SIZE_MB = 0.001

        doc = Document()
        large_file = tmp_path / "large.docx"
        doc.save(large_file)

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

        translator.MAX_FILE_SIZE_MB = 50

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

        doc = Document()
        doc.add_paragraph("Test")
        valid_file = tmp_path / "valid.docx"
        doc.save(valid_file)

        translator._validate_file(valid_file)


class TestParagraphTranslation:
    """Tests for paragraph text translation (AC1)."""

    def test_translate_paragraph_runs(self, tmp_path):
        """Adjacent runs with identical formatting merge into ONE unit.

        "Hello" + " " + "World" (same formatting, rsid-style splits) must be
        translated as the whole sentence, not as separate fragments.
        """
        mock_provider = MockTranslationProvider(
            {
                "Hello World": "Bonjour le monde",
            }
        )
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        para = doc.add_paragraph()
        para.add_run("Hello")
        para.add_run(" ")
        para.add_run("World")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)
        text = doc_out.paragraphs[0].text

        assert text == "Bonjour le monde"
        # One merged unit → a single provider call
        assert mock_provider._call_count == 1

    def test_bold_span_kept_separate_and_coherent(self, tmp_path):
        """A formatting change mid-sentence splits the units; spaces survive."""
        mock_provider = MockTranslationProvider(
            {
                "This is": "Ceci est",
                "very important": "très important",
            }
        )
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        para = doc.add_paragraph()
        para.add_run("This is ")
        bold = para.add_run("very important")
        bold.bold = True

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)
        text = doc_out.paragraphs[0].text
        assert text == "Ceci est très important"

        # Bold formatting survives on the right span
        runs = [r for r in doc_out.paragraphs[0].runs if r.text.strip()]
        assert len(runs) == 2
        assert runs[1].bold is True
        assert runs[1].text == "très important"

    def test_empty_paragraphs_not_translated(self, tmp_path):
        """Test that empty paragraphs are not translated."""
        mock_provider = MockTranslationProvider()
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        doc.add_paragraph("")
        doc.add_paragraph("   ")
        doc.add_paragraph("Text")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        assert mock_provider._call_count == 1


class TestFormattingPreservation:
    """Tests for formatting preservation (AC2, AC5)."""

    def test_run_formatting_preserved(self, tmp_path):
        """Test that run formatting is preserved after translation."""
        mock_provider = MockTranslationProvider({"Bold Text": "Texte Gras"})
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        para = doc.add_paragraph()
        run = para.add_run("Bold Text")
        run.bold = True
        run.italic = True
        run.font.size = Pt(14)

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)
        para_out = doc_out.paragraphs[0]
        run_out = para_out.runs[0]

        assert run_out.text == "Texte Gras"
        assert run_out.bold is True
        assert run_out.italic is True

    def test_paragraph_alignment_preserved(self, tmp_path):
        """Test that paragraph alignment is preserved."""
        mock_provider = MockTranslationProvider({"Centered": "Centre"})
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        para = doc.add_paragraph()
        para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = para.add_run("Centered")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)
        para_out = doc_out.paragraphs[0]

        assert para_out.alignment == WD_ALIGN_PARAGRAPH.CENTER


class TestTableTranslation:
    """Tests for table cell translation (AC3)."""

    def test_table_cells_translated(self, tmp_path):
        """Test that table cell text is translated."""
        mock_provider = MockTranslationProvider(
            {
                "Header": "En-tete",
                "Cell": "Cellule",
            }
        )
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        table = doc.add_table(rows=2, cols=2)
        table.cell(0, 0).text = "Header"
        table.cell(0, 1).text = "Header"
        table.cell(1, 0).text = "Cell"
        table.cell(1, 1).text = "Cell"

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)
        table_out = doc_out.tables[0]

        assert table_out.cell(0, 0).text == "En-tete"
        assert table_out.cell(1, 0).text == "Cellule"

    def test_nested_tables_translated(self, tmp_path):
        """Test that nested table text is translated."""
        mock_provider = MockTranslationProvider({"Nested": "Imbrique"})
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        table = doc.add_table(rows=1, cols=1)
        cell = table.cell(0, 0)
        cell.text = "Nested"

        nested_table = cell.add_table(rows=1, cols=1)
        nested_table.cell(0, 0).text = "Nested"

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)
        assert mock_provider._call_count == 2


class TestHeaderFooterTranslation:
    """Tests for header/footer translation (AC4)."""

    def test_header_translated(self, tmp_path):
        """Test that header text is translated."""
        mock_provider = MockTranslationProvider({"Header Text": "Texte En-tete"})
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        section = doc.sections[0]
        header = section.header
        header_para = header.paragraphs[0]
        header_para.text = "Header Text"

        doc.add_paragraph("Body text")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)
        header_out = doc_out.sections[0].header

        assert "Texte En-tete" in header_out.paragraphs[0].text

    def test_footer_translated(self, tmp_path):
        """Test that footer text is translated."""
        mock_provider = MockTranslationProvider({"Footer Text": "Texte Pied"})
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        section = doc.sections[0]
        footer = section.footer
        footer_para = footer.paragraphs[0]
        footer_para.text = "Footer Text"

        doc.add_paragraph("Body text")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)
        footer_out = doc_out.sections[0].footer

        assert "Texte Pied" in footer_out.paragraphs[0].text


class TestProgressCallback:
    """Tests for progress callback (NFR3)."""

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

        doc = Document()
        doc.add_paragraph("Test")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(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 "paragraph" in progress_events[0]
        assert "total_paragraphs" 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 = WordTranslator(provider=mock_provider)

        doc = Document()
        doc.add_paragraph("Test")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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


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

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

        doc = Document()
        doc.add_paragraph("Hello")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(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 = WordTranslator(provider=mock_provider)
        translator.set_custom_prompt("Translate to formal French")

        doc = Document()
        doc.add_paragraph("Hello")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(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 (AC7)."""

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

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

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

        assert exc_info.value.code in [
            WordProcessorError.DOCX_CORRUPTED,
            WordProcessorError.DOCX_READ_ERROR,
        ]

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

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

        with pytest.raises(WordProcessorError) 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 = WordTranslator()

        doc = Document()
        doc.add_paragraph("Hello")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(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 word_translator instance exists."""
        from translators import word_translator

        assert word_translator is not None
        assert isinstance(word_translator, WordTranslator)


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

    def test_images_preserved_in_output(self, tmp_path):
        """Test that images are preserved during translation (AC3)."""
        mock_provider = MockTranslationProvider({"Test": "TestFR", "Cell": "Cellule"})
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        doc.add_paragraph("Test")
        table = doc.add_table(rows=1, cols=1)
        table.cell(0, 0).text = "Cell"
        para_with_image = doc.add_paragraph()
        run = para_with_image.add_run()
        run.add_picture = lambda *args, **kwargs: None

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        import zipfile

        with zipfile.ZipFile(input_file, "r") as zf_in:
            input_media = [f for f in zf_in.namelist() if f.startswith("word/media/")]

        with zipfile.ZipFile(output_file, "r") as zf_out:
            output_media = [f for f in zf_out.namelist() if f.startswith("word/media/")]

        assert len(input_media) == len(output_media), "Image files should be preserved"

    def test_image_positions_preserved(self, tmp_path):
        """Test that image positions remain unchanged (AC3)."""
        mock_provider = MockTranslationProvider({"Before": "Avant", "After": "Apres"})
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        doc.add_paragraph("Before")
        para_with_image = doc.add_paragraph()
        run = para_with_image.add_run()
        run.add_picture = lambda *args, **kwargs: None
        doc.add_paragraph("After")

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)
        assert len(doc_out.paragraphs) >= 3

        assert "Avant" in doc_out.paragraphs[0].text
        assert "Apres" in doc_out.paragraphs[2].text


class TestWriteErrorHandling:
    """Tests for write error scenarios."""

    def test_write_to_readonly_location(self, tmp_path):
        """Test that write error is raised with proper code."""
        mock_provider = MockTranslationProvider({"Test": "TestFR"})
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        doc.add_paragraph("Test")
        input_file = tmp_path / "input.docx"
        doc.save(input_file)

        # Create a file instead of a directory to force a write error on both Windows and Unix
        readonly_dir = tmp_path / "readonly_file"
        readonly_dir.write_text("blocked")
        output_file = readonly_dir / "output.docx"

        with pytest.raises(WordProcessorError) as exc_info:
            translator.translate_file(input_file, output_file, "fr")
        assert exc_info.value.code == WordProcessorError.DOCX_WRITE_ERROR


class TestMultipleSections:
    """Tests for documents with multiple sections."""

    def test_multiple_sections_headers_translated(self, tmp_path):
        """Test that section headers/footers are translated."""
        mock_provider = MockTranslationProvider(
            {
                "Header1": "EnTete1",
                "Header2": "EnTete2",
                "Footer1": "Pied1",
                "Footer2": "Pied2",
            }
        )
        translator = WordTranslator(provider=mock_provider)

        doc = Document()

        section1 = doc.sections[0]
        section1.header.paragraphs[0].text = "Header1"
        section1.footer.paragraphs[0].text = "Footer1"

        from docx.enum.section import WD_ORIENT

        new_section = doc.add_section()
        new_section.header.is_linked_to_previous = False
        new_section.header.paragraphs[0].text = "Header2"
        new_section.footer.is_linked_to_previous = False
        new_section.footer.paragraphs[0].text = "Footer2"

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)
        assert len(doc_out.sections) == 2
        assert (
            "EnTete1" in doc_out.sections[0].header.paragraphs[0].text
            or "EnTete2" in doc_out.sections[0].header.paragraphs[0].text
        )
        assert "EnTete2" in doc_out.sections[1].header.paragraphs[0].text


class TestDocxCompatibility:
    """Tests for docx compatibility (AC6)."""

    def test_valid_docx_structure(self, tmp_path):
        """Test that output file has valid docx structure that Word can open."""
        mock_provider = MockTranslationProvider({"Test": "TestFR"})
        translator = WordTranslator(provider=mock_provider)

        doc = Document()
        doc.add_paragraph("Test")
        table = doc.add_table(rows=1, cols=1)
        table.cell(0, 0).text = "Cell"

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        import zipfile

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

        doc_out = Document(output_file)
        assert doc_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("word/document.xml" in f for f in files), "Missing document.xml"

    def test_complex_document_compatibility(self, tmp_path):
        """Test compatibility with complex documents containing tables, headers, and formatting."""
        mock_provider = MockTranslationProvider(
            {
                "Title": "Titre",
                "Content": "Contenu",
                "Header": "En-tete",
            }
        )
        translator = WordTranslator(provider=mock_provider)

        doc = Document()

        title = doc.add_heading("Title", level=1)

        doc.add_paragraph("Content")

        table = doc.add_table(rows=2, cols=2)
        table.cell(0, 0).text = "Header"
        table.cell(1, 1).text = "Content"

        section = doc.sections[0]
        section.header.paragraphs[0].text = "Header"

        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

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

        doc_out = Document(output_file)

        assert len(doc_out.paragraphs) >= 2
        assert len(doc_out.tables) == 1
