"""
Tests for Track B2 — PowerPoint format preservation fixes.

Covers:
  B2.1 — Placeholder filter (DATE=16, SLIDE_NUMBER=13, FOOTER=15)
         PowerPoint auto-fills these at render time, so translating them
         is pointless and just wastes API calls.
  B2.2 — SmartArt diagram text collection (ppt/diagrams/data*.xml)
  B2.3 — SmartArt diagram translation applied via ZIP rewrite
  B2.4 — Chart text matched by element path (not string equality)
  B2.5 — Whitespace preservation in chart apply

These tests are intentionally narrow: they validate that a specific
bug is fixed. Existing tests in test_pptx_translator.py handle the
broader surface.
"""
import os
import tempfile
import zipfile
from pathlib import Path
from typing import List, Dict
from unittest.mock import MagicMock, patch

import pytest
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.shapes import MSO_SHAPE_TYPE
from lxml import etree

from translators.pptx_translator import PowerPointTranslator, pptx_translator


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

class MockProvider:
    """Mock translation provider: returns 'TR_<text>' unless a mapping
    is provided."""

    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:
                out.append(t)
        return out

    def get_name(self):
        return "mock"

    def is_available(self):
        return True


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

_NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main"
_NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart"
_NS_P = "http://schemas.openxmlformats.org/presentationml/2006/main"


def _add_textbox_with_text(slide, text: str):
    tb = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(3), Inches(1))
    tf = tb.text_frame
    tf.text = text
    return tb


def _inject_diagram_part(pptx_path: Path, diagram_filename: str, diagram_xml: bytes):
    """Inject a SmartArt diagram XML part into a .pptx file."""
    tmp_out = pptx_path.with_suffix(".tmp")
    with zipfile.ZipFile(pptx_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 diagram data part
        existing[diagram_filename] = diagram_xml

        # Register in [Content_Types].xml
        ct_path = "[Content_Types].xml"
        if ct_path in existing:
            ct_content = existing[ct_path].decode("utf-8")
            override = (
                f'<Override PartName="/{diagram_filename}" '
                f'ContentType="application/vnd.openxmlformats-officedocument.'
                f'drawingml+xml+diagram"/>'
            )
            if override not in ct_content:
                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(pptx_path)


def _inject_chart_part(pptx_path: Path, chart_filename: str, chart_xml: bytes):
    """Inject a chart XML part into a .pptx file."""
    tmp_out = pptx_path.with_suffix(".tmp")
    with zipfile.ZipFile(pptx_path, "r") as zin, \
         zipfile.ZipFile(tmp_out, "w", zipfile.ZIP_DEFLATED) as zout:
        existing = {item: zin.read(item) for item in zin.namelist()}

        existing[chart_filename] = chart_xml

        ct_path = "[Content_Types].xml"
        if ct_path in existing:
            ct_content = existing[ct_path].decode("utf-8")
            override = (
                f'<Override PartName="/{chart_filename}" '
                f'ContentType="application/vnd.openxmlformats-officedocument.'
                f'drawingml+xml+chart"/>'
            )
            if override not in ct_content:
                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(pptx_path)


# ============================================================================
# B2.1 — Placeholder filter (DATE / SLIDE_NUMBER / FOOTER)
# ============================================================================

class TestPptxPlaceholderFilter:
    """Bug fix: PowerPoint auto-generates DATE, SLIDE_NUMBER, and FOOTER
    placeholders at render time. Translating them wastes API calls
    because the translated value gets overwritten on next render."""

    def _build_pptx_with_placeholders(self, tmp_path: Path) -> Path:
        """Build a PPTX with a date placeholder, a slide-number placeholder,
        a footer placeholder, plus one real text box. Returns the path."""
        from pptx import Presentation

        prs = Presentation()
        slide_layout = prs.slide_layouts[6]  # blank
        slide = prs.slides.add_slide(slide_layout)

        # Add real text — should be translated
        _add_textbox_with_text(slide, "Real text")

        # python-pptx doesn't expose a direct "add footer/date/sldNum
        # placeholder" API, so we inject the XML directly. The placeholder
        # types that should be filtered:
        #   13 (sldNum), 15 (ftr), 16 (dt)
        from pptx.oxml.ns import qn
        from lxml import etree as _etree

        spTree = slide.shapes._spTree
        nvSpPr_template = """
        <p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
              xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
          <p:nvSpPr>
            <p:nvPr>
              <p:ph type="{ph_type}" idx="1"/>
            </p:nvPr>
            <p:cNvPr id="{shape_id}" name="PH{shape_id}"/>
            <p:cNvSpPr/>
          </p:nvSpPr>
          <p:spPr>
            <a:xfrm>
              <a:off x="0" y="0"/>
              <a:ext cx="1000000" cy="500000"/>
            </a:xfrm>
          </p:spPr>
          <p:txBody>
            <a:bodyPr/>
            <a:lstStyle/>
            <a:p>
              <a:r>
                <a:rPr lang="en-US"/>
                <a:t>{text}</a:t>
              </a:r>
            </a:p>
          </p:txBody>
        </p:sp>
        """

        # Footer placeholder (type 15)
        sp_xml = nvSpPr_template.format(
            ph_type="ftr",
            shape_id=100,
            text="Footer auto",
        )
        sp_elem = _etree.fromstring(sp_xml)
        spTree.append(sp_elem)

        # Date placeholder (type 16)
        sp_xml = nvSpPr_template.format(
            ph_type="dt",
            shape_id=101,
            text="Date auto",
        )
        sp_elem = _etree.fromstring(sp_xml)
        spTree.append(sp_elem)

        # Slide number placeholder (type 13)
        sp_xml = nvSpPr_template.format(
            ph_type="sldNum",
            shape_id=102,
            text="Slide number auto",
        )
        sp_elem = _etree.fromstring(sp_xml)
        spTree.append(sp_elem)

        path = tmp_path / "placeholders.pptx"
        prs.save(path)
        return path

    def test_placeholders_skipped(self, tmp_path):
        """The three auto-generated placeholder types should NOT be sent
        to the translation provider."""
        provider = MockProvider({"Real text": "TR_Real text"})
        translator = PowerPointTranslator(provider=provider)

        input_file = self._build_pptx_with_placeholders(tmp_path)
        output_file = tmp_path / "output.pptx"

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

        # Real text should be translated
        assert "Real text" in provider.calls, (
            f"Real text missing from provider calls: {provider.calls}"
        )

        # Auto-generated placeholders should NOT be in the calls
        assert "Footer auto" not in provider.calls, (
            f"Footer placeholder was sent to provider: {provider.calls}"
        )
        assert "Date auto" not in provider.calls, (
            f"Date placeholder was sent to provider: {provider.calls}"
        )
        assert "Slide number auto" not in provider.calls, (
            f"Slide number placeholder was sent to provider: {provider.calls}"
        )


# ============================================================================
# B2.2 — SmartArt diagram text collection
# ============================================================================

class TestPptxDiagramTextCollection:
    """New feature: SmartArt text (in ppt/diagrams/data*.xml) is now
    collected and translated."""

    DIAGRAM_XML = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<dgm:dataModel xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram"
               xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
               xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <dgm:ptLst>
    <dgm:pt modelId="0" type="doc">
      <dgm:prSet loTypeId="urn:microsoft.com/office/officeart/2005/8/layout/hierarchy1" loCatId="hierarchy"/>
      <dgm:t>
        <a:bodyPr/>
        <a:lstStyle/>
        <a:p>
          <a:r>
            <a:rPr lang="en-US"/>
            <a:t>Root node</a:t>
          </a:r>
        </a:p>
      </dgm:t>
    </dgm:pt>
    <dgm:pt modelId="1">
      <dgm:t>
        <a:bodyPr/>
        <a:lstStyle/>
        <a:p>
          <a:r>
            <a:rPr lang="en-US"/>
            <a:t>Child one</a:t>
          </a:r>
        </a:p>
      </dgm:t>
    </dgm:pt>
    <dgm:pt modelId="2">
      <dgm:t>
        <a:bodyPr/>
        <a:lstStyle/>
        <a:p>
          <a:r>
            <a:rPr lang="en-US"/>
            <a:t>Child two</a:t>
          </a:r>
        </a:p>
      </dgm:t>
    </dgm:pt>
  </dgm:ptLst>
</dgm:dataModel>
"""

    def _build_pptx_with_diagram(self, tmp_path: Path) -> Path:
        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[6])
        _add_textbox_with_text(slide, "Hello")
        path = tmp_path / "with_diagram.pptx"
        prs.save(path)
        _inject_diagram_part(
            path, "ppt/diagrams/data1.xml",
            self.DIAGRAM_XML.encode("utf-8"),
        )
        return path

    def test_diagram_text_collected(self, tmp_path):
        """Diagram text should appear in provider calls."""
        provider = MockProvider()
        translator = PowerPointTranslator(provider=provider)

        input_file = self._build_pptx_with_diagram(tmp_path)
        output_file = tmp_path / "output.pptx"

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

        # All three diagram nodes should be collected
        assert "Root node" in provider.calls, (
            f"Diagram 'Root node' not collected. Calls: {provider.calls}"
        )
        assert "Child one" in provider.calls, (
            f"Diagram 'Child one' not collected. Calls: {provider.calls}"
        )
        assert "Child two" in provider.calls, (
            f"Diagram 'Child two' not collected. Calls: {provider.calls}"
        )

    def test_diagram_text_applied(self, tmp_path):
        """Translated diagram text should be written back to the .pptx
        ZIP (in ppt/diagrams/data*.xml)."""
        provider = MockProvider({
            "Root node": "TR_Root node",
            "Child one": "TR_Child one",
            "Child two": "TR_Child two",
        })
        translator = PowerPointTranslator(provider=provider)

        input_file = self._build_pptx_with_diagram(tmp_path)
        output_file = tmp_path / "output.pptx"

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

        # The diagram part in the output ZIP should have the translated text
        with zipfile.ZipFile(output_file, "r") as zf:
            assert "ppt/diagrams/data1.xml" in zf.namelist(), (
                f"Diagram part missing in output. Files: {zf.namelist()}"
            )
            data = zf.read("ppt/diagrams/data1.xml").decode("utf-8")
        assert "TR_Root node" in data, (
            f"Diagram 'Root node' not translated in output:\n{data[:1000]}"
        )
        assert "TR_Child one" in data, (
            f"Diagram 'Child one' not translated in output"
        )
        assert "TR_Child two" in data, (
            f"Diagram 'Child two' not translated in output"
        )

    def test_no_diagram_no_error(self, tmp_path):
        """A PPTX without a diagram part should not raise."""
        provider = MockProvider({"Hello": "TR_Hello"})
        translator = PowerPointTranslator(provider=provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[6])
        _add_textbox_with_text(slide, "Hello")
        input_file = tmp_path / "no_diagram.pptx"
        output_file = tmp_path / "output.pptx"
        prs.save(input_file)

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


# ============================================================================
# B2.3 — Diagram whitespace preservation
# ============================================================================

class TestPptxDiagramWhitespace:
    """Whitespace around translated diagram text should be preserved."""

    DIAGRAM_PADDED_XML = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<dgm:dataModel xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram"
               xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
  <dgm:ptLst>
    <dgm:pt modelId="0">
      <dgm:t>
        <a:bodyPr/>
        <a:lstStyle/>
        <a:p>
          <a:r>
            <a:rPr lang="en-US"/>
            <a:t>  Padded text  </a:t>
          </a:r>
        </a:p>
      </dgm:t>
    </dgm:pt>
  </dgm:ptLst>
</dgm:dataModel>
"""

    def test_padded_diagram_text_preserved(self, tmp_path):
        provider = MockProvider({"Padded text": "TR_Padded text"})
        translator = PowerPointTranslator(provider=provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[6])
        input_file = tmp_path / "padded.pptx"
        prs.save(input_file)
        _inject_diagram_part(
            input_file, "ppt/diagrams/data1.xml",
            self.DIAGRAM_PADDED_XML.encode("utf-8"),
        )
        output_file = tmp_path / "output.pptx"

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

        with zipfile.ZipFile(output_file, "r") as zf:
            data = zf.read("ppt/diagrams/data1.xml").decode("utf-8")
        # Leading/trailing spaces should be preserved
        assert "  TR_Padded text  " in data, (
            f"Whitespace not preserved in diagram translation:\n{data[:1000]}"
        )


# ============================================================================
# B2.4 — Element path navigation
# ============================================================================

class TestPptxElementPathNavigation:
    """Bug fix: chart/diagram elements should be matched by stored
    element_path, not by string equality. This means identical text
    in multiple elements (e.g. two 'Revenue' series) is handled correctly."""

    SIMPLE_DIAGRAM = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
  <a:t>Alpha</a:t>
  <a:t>Beta</a:t>
  <a:t>Gamma</a:t>
</root>
"""

    def test_path_navigation_basic(self):
        """The `_get_element_path` / `_find_element_by_path` pair should
        round-trip correctly through a small XML tree."""
        root = etree.fromstring(self.SIMPLE_DIAGRAM.encode("utf-8"))
        translator = PowerPointTranslator()

        # Get paths for all three <a:t> elements
        elements = list(root.iter(f"{{{_NS_A}}}t"))
        assert len(elements) == 3

        paths = [translator._get_element_path(e) for e in elements]

        # All three paths should be unique
        assert len(set(paths)) == 3, f"Paths not unique: {paths}"

        # Each path should resolve back to the same element
        for elem, path in zip(elements, paths):
            found = translator._find_element_by_path(root, path)
            assert found is not None, f"Path didn't resolve: {path}"
            assert found.text == elem.text, (
                f"Path {path} resolved to wrong element: "
                f"{found.text!r} vs {elem.text!r}"
            )

    def test_path_fallback_to_string_match(self):
        """If the stored path is empty, the apply step should fall back
        to string equality on `entry['original']`."""
        root = etree.fromstring(self.SIMPLE_DIAGRAM.encode("utf-8"))
        translator = PowerPointTranslator()

        # Simulate a legacy entry with no element_path
        legacy_entry = {
            "original": "Beta",
            "translated": "TR_Beta",
            "element_path": None,  # legacy
        }

        # Simulate the apply logic
        target = None
        for candidate in root.iter(f"{{{_NS_A}}}t"):
            if (candidate.text or "").strip() == legacy_entry["original"]:
                target = candidate
                break
        assert target is not None, "Fallback string match didn't find Beta"
        assert target.text == "Beta"

    def test_diagram_with_duplicate_text_paths_differ(self, tmp_path):
        """Two diagram nodes with the same text should have different
        element_path values, so the apply step can target them correctly."""
        dup_xml = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<dgm:dataModel xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram"
               xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
  <dgm:ptLst>
    <dgm:pt modelId="0">
      <dgm:t><a:p><a:r><a:t>Revenue</a:t></a:r></a:p></dgm:t>
    </dgm:pt>
    <dgm:pt modelId="1">
      <dgm:t><a:p><a:r><a:t>Revenue</a:t></a:r></a:p></dgm:t>
    </dgm:pt>
  </dgm:ptLst>
</dgm:dataModel>
"""
        provider = MockProvider()
        translator = PowerPointTranslator(provider=provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[6])
        input_file = tmp_path / "dup.pptx"
        prs.save(input_file)
        _inject_diagram_part(
            input_file, "ppt/diagrams/data1.xml",
            dup_xml.encode("utf-8"),
        )
        output_file = tmp_path / "output.pptx"

        # Should not raise; both should be translated independently
        translator.translate_file(input_file, output_file, "fr")

        # Both 'Revenue' texts should be in the calls
        assert provider.calls.count("Revenue") == 2, (
            f"Expected 2 'Revenue' calls, got {provider.calls.count('Revenue')}. "
            f"All calls: {provider.calls}"
        )


# ============================================================================
# B2.5 — Chart whitespace preservation
# ============================================================================

class TestPptxChartWhitespace:
    """Whitespace around translated chart text should be preserved."""

    CHART_PADDED_XML = f"""<?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>  Padded chart title  </a:t>
            </a:r>
          </a:p>
        </c:rich>
      </c:tx>
    </c:title>
  </c:chart>
</c:chartSpace>
"""

    def test_chart_translation_reaches_output_file(self, tmp_path):
        """Chart translations must reach the OUTPUT FILE (real ChartPart).

        python-pptx's ChartPart.blob is read-only — the previous in-memory
        `blob = ...` write never landed in the saved .pptx. This end-to-end
        test uses a real chart and verifies the chart XML inside the
        output ZIP.
        """
        from pptx.chart.data import CategoryChartData
        from pptx.enum.chart import XL_CHART_TYPE

        provider = MockProvider({"Padded chart title": "Titre avec espaces"})
        translator = PowerPointTranslator(provider=provider)

        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[5])
        chart_data = CategoryChartData()
        chart_data.categories = ["A", "B"]
        chart_data.add_series("Series 1", (1, 2))
        graphic_frame = slide.shapes.add_chart(
            XL_CHART_TYPE.COLUMN_CLUSTERED, 10, 10, 400, 300, chart_data
        )
        chart = graphic_frame.chart
        chart.has_title = True
        chart.chart_title.text_frame.text = "Padded chart title"

        input_file = tmp_path / "chart_in.pptx"
        output_file = tmp_path / "chart_out.pptx"
        prs.save(str(input_file))

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

        with zipfile.ZipFile(output_file, "r") as zf:
            chart_parts = [
                n for n in zf.namelist() if n.startswith("ppt/charts/chart")
            ]
            assert chart_parts, "chart part missing from output file"
            chart_xml = zf.read(chart_parts[0]).decode("utf-8")

        assert "Titre avec espaces" in chart_xml, (
            "Chart title translation never reached the output file"
        )
        assert "Padded chart title" not in chart_xml
