"""
Tests for Track B1 — Format preservation quick wins (Word + Excel).

Covers:
  W1 — Word hyperlink double-collect fix
  W2 — Word footnotes import fix
  W4 — Word chart matching by XPath (no string equality)
  E1 — (deferred — rich text in cells is risky)
  E2 — Excel cell comments
  E3 — Excel cell hyperlinks (URL preserved, label translated)
  E4 — Excel chart matching by XPath

These tests are intentionally narrow: they validate that a specific
bug is fixed. They do NOT cover the full translator surface (existing
tests in test_word_translator.py and test_excel_translator.py handle that).
"""
import os
import tempfile
import zipfile
from pathlib import Path
from typing import List, Tuple, Callable, Dict

import pytest
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from openpyxl import Workbook
from openpyxl.comments import Comment
from openpyxl.worksheet.hyperlink import Hyperlink

from translators.word_translator import WordTranslator
from translators.excel_translator import ExcelTranslator


# ---------------- Helpers ----------------

def _add_hyperlink_to_paragraph(paragraph, url: str, text: str) -> None:
    """Add a hyperlink to a python-docx paragraph (low-level API)."""
    part = paragraph.part
    r_id = part.relate_to(url, RT.HYPERLINK, is_external=True)
    hyperlink = OxmlElement("w:hyperlink")
    hyperlink.set(qn("r:id"), r_id)

    new_run = OxmlElement("w:r")
    rPr = OxmlElement("w:rPr")
    # Mark as hyperlink style
    rStyle = OxmlElement("w:rStyle")
    rStyle.set(qn("w:val"), "Hyperlink")
    rPr.append(rStyle)
    new_run.append(rPr)

    new_text = OxmlElement("w:t")
    new_text.text = text
    new_text.set(qn("xml:space"), "preserve")
    new_run.append(new_text)

    hyperlink.append(new_run)
    paragraph._p.append(hyperlink)


# ---------------- Mock provider ----------------

class MockProvider:
    """Mock translation provider: maps source text to a TR_<text> translation."""

    def __init__(self, translations: Dict[str, str] = None):
        self._translations = translations or {}
        self.calls: List[str] = []

    def translate_batch(
        self, texts: List[str], target_language: str = "fr", source_language: str = "auto"
    ) -> List[str]:
        out = []
        for t in texts:
            self.calls.append(t)
            if t in self._translations:
                out.append(self._translations[t])
            elif t.startswith("TR_"):
                out.append(t)
            else:
                # Default: a no-op translation
                out.append(t)
        return out

    def get_name(self):
        return "mock"

    def is_available(self):
        return True


def _make_tmp(suffix: str) -> Path:
    fd, name = tempfile.mkstemp(suffix=suffix)
    os.close(fd)
    return Path(name)


# ============================================================================
# W1 — Word hyperlink double-collect
# ============================================================================

class TestWordHyperlinkNoDoubleCollect:
    """Bug fix: a run inside a <w:hyperlink> used to be collected twice
    (once via paragraph.runs, once via the hyperlink iter loop), wasting
    API calls and potentially causing double-apply on save."""

    def test_hyperlink_run_translated_once(self, tmp_path):
        provider = MockProvider({"Click here": "TR_Click here"})
        translator = WordTranslator(provider=provider)

        doc = Document()
        # Add a hyperlink to a paragraph (low-level API)
        p = doc.add_paragraph("See ")
        _add_hyperlink_to_paragraph(p, "https://example.com", "Click here")
        doc.add_paragraph(" for details")

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

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

        # "Click here" should appear in the provider calls at most once.
        click_calls = [c for c in provider.calls if c == "Click here"]
        assert len(click_calls) == 1, (
            f"Hyperlink run was collected {len(click_calls)} times, expected 1. "
            f"All calls: {provider.calls}"
        )

    def test_hyperlink_url_preserved(self, tmp_path):
        """The hyperlink target URL should be preserved (not translated)."""
        provider = MockProvider({"Click here": "Cliquez ici"})
        translator = WordTranslator(provider=provider)

        doc = Document()
        p = doc.add_paragraph("Visit ")
        _add_hyperlink_to_paragraph(p, "https://example.com", "Click here")

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

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

        # The visible text should be translated
        output_doc = Document(output_file)
        assert "Cliquez ici" in str(output_doc._body._element.xml)

        # The URL itself is stored in the .rels file, not the body.
        # Check the relationships for the URL.
        with zipfile.ZipFile(output_file, "r") as zf:
            rels = zf.read("word/_rels/document.xml.rels").decode("utf-8")
        assert "https://example.com" in rels, (
            f"Hyperlink URL not found in rels file. rels:\n{rels}"
        )

    def test_multiple_hyperlinks(self, tmp_path):
        """Multiple distinct hyperlinks should each be translated once."""
        provider = MockProvider({
            "First link": "Premier lien",
            "Second link": "Second lien",
        })
        translator = WordTranslator(provider=provider)

        doc = Document()
        p1 = doc.add_paragraph()
        _add_hyperlink_to_paragraph(p1, "https://a.com", "First link")
        p2 = doc.add_paragraph()
        _add_hyperlink_to_paragraph(p2, "https://b.com", "Second link")

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

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

        # Each hyperlink text should appear at most once
        assert provider.calls.count("First link") == 1
        assert provider.calls.count("Second link") == 1


# ============================================================================
# W2 — Word footnotes import
# ============================================================================

class TestWordFootnotesCollected:
    """Bug fix: `_collect_from_footnotes` used `document.part.package`
    which doesn't exist in python-docx 1.x — so footnotes were never
    translated. Now it uses `document.part.related_parts` to find the
    footnotes part by content type."""

    def test_footnotes_translated(self, tmp_path):
        provider = MockProvider({
            "Footnote text": "Texte de note",
            "Body text": "Texte du corps",
        })
        translator = WordTranslator(provider=provider)

        # Build a document with a footnote
        doc = Document()
        p = doc.add_paragraph("Body text")
        # python-docx doesn't have a high-level API for footnotes, so we
        # inject the XML directly
        from docx.oxml.ns import qn
        from docx.oxml import OxmlElement

        # Create a footnote reference in the paragraph
        run = p.add_run()
        footnote_ref = OxmlElement("w:footnoteReference")
        footnote_ref.set(qn("w:id"), "1")
        run._r.append(footnote_ref)

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

        # Manually inject a footnotes.xml part into the docx
        footnotes_xml = b"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:footnote w:id="1">
    <w:p>
      <w:r>
        <w:t>Footnote text</w:t>
      </w:r>
    </w:p>
  </w:footnote>
</w:footnotes>"""

        _inject_xml_part(
            input_file, "word/footnotes.xml", footnotes_xml,
            content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
        )
        _add_relationship(input_file, "rIdFootnotes",
                          "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",
                          "footnotes.xml")

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

        # Both body and footnote text should be in the provider calls
        assert "Body text" in provider.calls
        assert "Footnote text" in provider.calls, (
            f"Footnote text was not collected. All calls: {provider.calls}"
        )

    def test_no_footnotes_part_no_error(self, tmp_path):
        """A document without footnotes should not raise an error."""
        provider = MockProvider({"Hello": "Bonjour"})
        translator = WordTranslator(provider=provider)

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

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

        # Should not raise even though the doc has no footnotes part
        translator.translate_file(input_file, output_file, "fr")
        # Sanity check
        output_doc = Document(output_file)
        assert output_doc.paragraphs[0].text == "Bonjour"


# ============================================================================
# W4 — Word chart matching by XPath
# ============================================================================

class TestWordChartXPathMatching:
    """Bug fix: chart translation matched by string equality, so if the
    same text appeared in multiple charts (e.g. two "Revenue" series),
    only the first was translated. Now we use the element path collected
    at gather time."""

    def test_chart_in_zip_gets_translated(self, tmp_path):
        """A docx with a chart containing text should have the chart text
        translated via the chart apply path."""
        # The full chart-pipeline test is in the original test suite;
        # here we just verify the new path-based code doesn't crash.
        provider = MockProvider({"Chart title": "Titre du graphique"})
        translator = WordTranslator(provider=provider)

        # Build a minimal docx that has a chart XML part
        doc = Document()
        doc.add_paragraph("Chart title")
        input_file = tmp_path / "input.docx"
        output_file = tmp_path / "output.docx"
        doc.save(input_file)

        # Inject a chart XML part so the collect path runs
        chart_xml = b"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"
              xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
  <c:chart>
    <c:title>
      <c:tx>
        <c:rich>
          <a:p>
            <a:r>
              <a:t>Chart title</a:t>
            </a:r>
          </a:p>
        </c:rich>
      </c:tx>
    </c:title>
  </c:chart>
</c:chartSpace>"""
        _inject_xml_part(input_file, "word/charts/chart1.xml", chart_xml)
        _add_relationship(input_file, "rIdChart1",
                          "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart",
                          "charts/chart1.xml")

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

        # The chart text should be among the collected texts
        assert "Chart title" in provider.calls


# ============================================================================
# E2 — Excel cell comments
# ============================================================================

class TestExcelCellComments:
    """New feature: cell comments are now collected and translated."""

    def test_cell_comment_translated(self, tmp_path):
        provider = MockProvider({"Old comment": "Ancien commentaire"})
        translator = ExcelTranslator(provider=provider)

        wb = Workbook()
        ws = wb.active
        ws["A1"] = "Cell value"
        ws["A1"].comment = Comment("Old comment", "Author")
        input_file = tmp_path / "input.xlsx"
        output_file = tmp_path / "output.xlsx"
        wb.save(input_file)
        wb.close()

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

        # Comment text should be in the provider calls
        assert "Old comment" in provider.calls

        # The output should have the translated comment
        from openpyxl import load_workbook
        wb_out = load_workbook(output_file)
        assert wb_out.active["A1"].comment is not None
        # The translation dict maps "Old comment" -> "Ancien commentaire"
        assert wb_out.active["A1"].comment.text == "Ancien commentaire"
        wb_out.close()

    def test_empty_comment_skipped(self, tmp_path):
        provider = MockProvider()
        translator = ExcelTranslator(provider=provider)

        wb = Workbook()
        ws = wb.active
        ws["A1"] = "Value"
        input_file = tmp_path / "input.xlsx"
        output_file = tmp_path / "output.xlsx"
        wb.save(input_file)
        wb.close()

        # Should not raise
        translator.translate_file(input_file, output_file, "fr")


# ============================================================================
# E3 — Excel cell hyperlinks
# ============================================================================

class TestExcelCellHyperlinks:
    """New feature: cell hyperlink DISPLAY labels are translated; the
    underlying URL is preserved."""

    def test_hyperlink_label_translated(self, tmp_path):
        provider = MockProvider({"Click here": "Cliquez ici"})
        translator = ExcelTranslator(provider=provider)

        wb = Workbook()
        ws = wb.active
        ws["A1"] = "Click here"
        ws["A1"].hyperlink = Hyperlink(ref="A1", target="https://example.com")
        input_file = tmp_path / "input.xlsx"
        output_file = tmp_path / "output.xlsx"
        wb.save(input_file)
        wb.close()

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

        # The display label should be translated
        from openpyxl import load_workbook
        wb_out = load_workbook(output_file)
        cell = wb_out.active["A1"]
        # The value (display label) should be translated
        assert cell.value == "TR_Click here" or cell.value == "Cliquez ici"
        # The URL should be preserved
        assert cell.hyperlink is not None
        assert cell.hyperlink.target == "https://example.com"
        wb_out.close()

    def test_hyperlink_with_url_as_value_not_double_translated(self, tmp_path):
        """If the cell value IS the URL, don't try to translate it again."""
        provider = MockProvider()
        translator = ExcelTranslator(provider=provider)

        wb = Workbook()
        ws = wb.active
        ws["A1"] = "https://example.com"
        ws["A1"].hyperlink = Hyperlink(ref="A1", target="https://example.com")
        input_file = tmp_path / "input.xlsx"
        output_file = tmp_path / "output.xlsx"
        wb.save(input_file)
        wb.close()

        # Should not raise
        translator.translate_file(input_file, output_file, "fr")


# ============================================================================
# E4 — Excel chart matching by XPath
# ============================================================================

class TestExcelChartXPathMatching:
    """Bug fix: same as W4 but for Excel. Chart elements are now matched
    by element path, not string equality."""

    def test_chart_text_collected(self, tmp_path):
        provider = MockProvider({"Chart title": "Titre graphique"})
        translator = ExcelTranslator(provider=provider)

        wb = Workbook()
        ws = wb.active
        ws["A1"] = "Cell value"
        input_file = tmp_path / "input.xlsx"
        output_file = tmp_path / "output.xlsx"
        wb.save(input_file)
        wb.close()

        # Inject a chart XML part
        chart_xml = b"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"
              xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
  <c:chart>
    <c:title>
      <c:tx>
        <c:rich>
          <a:p>
            <a:r>
              <a:t>Chart title</a:t>
            </a:r>
          </a:p>
        </c:rich>
      </c:tx>
    </c:title>
  </c:chart>
</c:chartSpace>"""
        _inject_xml_part(input_file, "xl/charts/chart1.xml", chart_xml)
        _add_relationship(input_file, "chart1",
                          "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart",
                          "charts/chart1.xml")

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

        # Chart text should be in the provider calls
        assert "Chart title" in provider.calls


# ============================================================================
# Helper functions
# ============================================================================

def _inject_xml_part(docx_path: Path, part_name: str, content: bytes,
                     content_type: str = None):
    """Inject a new XML part into an existing .docx or .xlsx file.

    If content_type is provided, also add a <Override> entry in
    [Content_Types].xml so the part is correctly identified. Without
    this, python-docx (and most Office parsers) will see the part as
    generic application/xml and won't find it via content_type lookups.
    """
    buf_bytes = docx_path.read_bytes()
    tmp_out = docx_path.with_suffix(".tmp")
    with zipfile.ZipFile(docx_path, "r") as zin, \
         zipfile.ZipFile(tmp_out, "w", zipfile.ZIP_DEFLATED) as zout:
        existing = {item: zin.read(item) for item in zin.namelist()}

        # Add the part
        existing[part_name] = content

        # Register the content type override if requested
        if content_type:
            ct_path = "[Content_Types].xml"
            if ct_path in existing:
                ct_content = existing[ct_path].decode("utf-8")
                # Insert a new <Override> before </Types>
                override = f'<Override PartName="/{part_name}" ContentType="{content_type}"/>'
                ct_content = ct_content.replace(
                    "</Types>", f"{override}</Types>"
                )
                existing[ct_path] = ct_content.encode("utf-8")

        for item, data in existing.items():
            zout.writestr(item, data)

    tmp_out.replace(docx_path)


def _add_relationship(docx_path: Path, rel_id: str, rel_type: str, target: str):
    """Add a relationship to word/_rels/document.xml.rels in a .docx file.
    The footnote/chart relationships for Word live in this file (they're
    document-level relationships, not part-level)."""
    tmp_out = docx_path.with_suffix(".tmp")
    rels_path = "word/_rels/document.xml.rels"

    with zipfile.ZipFile(docx_path, "r") as zin, \
         zipfile.ZipFile(tmp_out, "w", zipfile.ZIP_DEFLATED) as zout:
        existing = {item: zin.read(item) for item in zin.namelist()}

        if rels_path in existing:
            rels_content = existing[rels_path].decode("utf-8")
            new_rel_xml = f'<Relationship Id="{rel_id}" Type="{rel_type}" Target="{target}"/>'
            rels_content = rels_content.replace(
                "</Relationships>", f"{new_rel_xml}</Relationships>"
            )
            existing[rels_path] = rels_content.encode("utf-8")

        for item, data in existing.items():
            zout.writestr(item, data)

    tmp_out.replace(docx_path)
