"""Tests for element-level editing functions in pptx_mcp/engine/editor.py."""

from __future__ import annotations

import os
import struct
import tempfile

import pytest
from pptx import Presentation
from pptx.util import Inches, Pt

from pptx_mcp.engine.editor import (
    add_image,
    add_shape,
    add_textbox,
    align_elements,
    group_elements,
    list_elements,
    modify_element,
)
from pptx_mcp.models.editor import AlignmentType, ElementLocator, ElementModification


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


def _make_slide_with_shapes(
    shapes: list[dict] | None = None,
) -> tuple[Presentation, object]:
    """Create a Presentation with a blank slide containing specified shapes.

    Each shape dict has: left, top, width, height (in inches), and optional text.
    Returns (prs, slide).
    """
    prs = Presentation()
    slide = prs.slides.add_slide(prs.slide_layouts[6])  # blank layout

    for spec in shapes or []:
        txBox = slide.shapes.add_textbox(
            Inches(spec.get("left", 1)),
            Inches(spec.get("top", 1)),
            Inches(spec.get("width", 2)),
            Inches(spec.get("height", 1)),
        )
        tf = txBox.text_frame
        tf.word_wrap = True
        if "text" in spec:
            tf.paragraphs[0].text = spec["text"]
        if "name" in spec:
            txBox.name = spec["name"]
        if "font_size" in spec:
            for para in tf.paragraphs:
                for run in para.runs:
                    run.font.size = Pt(spec["font_size"])

    return prs, slide


def _save_and_reload(prs: Presentation) -> Presentation:
    """Save to temp file and reload to verify persistence."""
    with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as f:
        prs.save(f.name)
        return Presentation(f.name)


def _create_minimal_png(path: str) -> None:
    """Create a minimal valid 1x1 red PNG file."""
    # Minimal PNG: 8-byte sig + IHDR + IDAT + IEND
    import zlib

    sig = b'\x89PNG\r\n\x1a\n'

    # IHDR chunk
    ihdr_data = struct.pack('>IIBBBBB', 1, 1, 8, 2, 0, 0, 0)  # 1x1, 8bit RGB
    ihdr_crc = zlib.crc32(b'IHDR' + ihdr_data) & 0xffffffff
    ihdr = struct.pack('>I', 13) + b'IHDR' + ihdr_data + struct.pack('>I', ihdr_crc)

    # IDAT chunk (1x1 red pixel with filter byte)
    raw = b'\x00\xff\x00\x00'  # filter=none, R=255, G=0, B=0
    compressed = zlib.compress(raw)
    idat_crc = zlib.crc32(b'IDAT' + compressed) & 0xffffffff
    idat = struct.pack('>I', len(compressed)) + b'IDAT' + compressed + struct.pack('>I', idat_crc)

    # IEND chunk
    iend_crc = zlib.crc32(b'IEND') & 0xffffffff
    iend = struct.pack('>I', 0) + b'IEND' + struct.pack('>I', iend_crc)

    with open(path, 'wb') as f:
        f.write(sig + ihdr + idat + iend)


# ---------------------------------------------------------------------------
# TestListSlideElements
# ---------------------------------------------------------------------------


class TestListSlideElements:
    def test_list_elements_basic(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Hello"},
            {"left": 5, "top": 1, "width": 3, "height": 1, "text": "World"},
        ])
        elements = list_elements(slide)
        assert len(elements) == 2
        assert elements[0].index == 0
        assert elements[1].index == 1

    def test_list_elements_text_preview(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Some text content"},
        ])
        elements = list_elements(slide)
        assert elements[0].has_text is True
        assert elements[0].text_preview == "Some text content"

    def test_list_elements_positions(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 2.5, "top": 3.0, "width": 4.0, "height": 1.5},
        ])
        elements = list_elements(slide)
        el = elements[0]
        assert abs(el.left - 2.5) < 0.01
        assert abs(el.top - 3.0) < 0.01
        assert abs(el.width - 4.0) < 0.01
        assert abs(el.height - 1.5) < 0.01

    def test_list_elements_empty_slide(self):
        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[6])
        elements = list_elements(slide)
        assert elements == []


# ---------------------------------------------------------------------------
# TestModifyElement
# ---------------------------------------------------------------------------


class TestModifyElement:
    def test_modify_position(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Test"},
        ])
        result = modify_element(
            slide,
            ElementLocator(index=0),
            ElementModification(left=5.0, top=3.0),
        )
        assert abs(result.left - 5.0) < 0.01
        assert abs(result.top - 3.0) < 0.01

    def test_modify_size(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Test"},
        ])
        result = modify_element(
            slide,
            ElementLocator(index=0),
            ElementModification(width=6.0, height=2.0),
        )
        assert abs(result.width - 6.0) < 0.01
        assert abs(result.height - 2.0) < 0.01

    def test_modify_text(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Old text"},
        ])
        result = modify_element(
            slide,
            ElementLocator(index=0),
            ElementModification(text="New text"),
        )
        assert result.text_preview == "New text"

    def test_modify_font(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Styled"},
        ])
        result = modify_element(
            slide,
            ElementLocator(index=0),
            ElementModification(font_size=24, font_bold=True, font_color="#FF0000"),
        )
        # Font changes applied — verify via the shape directly
        shape = list(slide.shapes)[0]
        run = shape.text_frame.paragraphs[0].runs[0]
        assert run.font.bold is True
        assert abs(run.font.size / 12700 - 24) < 0.1

    def test_modify_fill(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Fill"},
        ])
        modify_element(
            slide,
            ElementLocator(index=0),
            ElementModification(fill_color="#0000FF"),
        )
        shape = list(slide.shapes)[0]
        assert str(shape.fill.fore_color.rgb) == "0000FF"

    def test_modify_border(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Border"},
        ])
        modify_element(
            slide,
            ElementLocator(index=0),
            ElementModification(border_color="#00FF00", border_width=2.0),
        )
        shape = list(slide.shapes)[0]
        assert str(shape.line.color.rgb) == "00FF00"
        assert abs(shape.line.width / 12700 - 2.0) < 0.1

    def test_modify_by_name(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "A", "name": "Title Box"},
            {"left": 5, "top": 1, "width": 3, "height": 1, "text": "B", "name": "Content Box"},
        ])
        result = modify_element(
            slide,
            ElementLocator(name="Content Box"),
            ElementModification(text="Updated B"),
        )
        assert result.text_preview == "Updated B"

    def test_modify_by_text_match(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Revenue Chart"},
            {"left": 5, "top": 1, "width": 3, "height": 1, "text": "Cost Chart"},
        ])
        result = modify_element(
            slide,
            ElementLocator(text_match="Revenue"),
            ElementModification(left=2.0),
        )
        assert abs(result.left - 2.0) < 0.01

    def test_modify_invalid_index(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Only one"},
        ])
        with pytest.raises(ValueError, match="out of range"):
            modify_element(
                slide,
                ElementLocator(index=5),
                ElementModification(left=1.0),
            )

    def test_modify_ambiguous_text(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Same text"},
            {"left": 5, "top": 1, "width": 3, "height": 1, "text": "Same text here"},
        ])
        with pytest.raises(ValueError, match="Ambiguous"):
            modify_element(
                slide,
                ElementLocator(text_match="Same text"),
                ElementModification(left=1.0),
            )

    def test_modify_partial_update(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 2, "width": 3, "height": 1, "text": "Partial"},
        ])
        # Only change left, verify top stays the same
        result = modify_element(
            slide,
            ElementLocator(index=0),
            ElementModification(left=5.0),
        )
        assert abs(result.left - 5.0) < 0.01
        assert abs(result.top - 2.0) < 0.01  # unchanged


# ---------------------------------------------------------------------------
# TestAddElement
# ---------------------------------------------------------------------------


class TestAddElement:
    def test_add_textbox(self):
        prs, slide = _make_slide_with_shapes([])
        result = add_textbox(slide, 1.0, 2.0, 4.0, 1.0, "New text box")
        assert result.has_text is True
        assert result.text_preview == "New text box"
        assert abs(result.left - 1.0) < 0.01

    def test_add_shape(self):
        prs, slide = _make_slide_with_shapes([])
        result = add_shape(
            slide, "rectangle", 2.0, 3.0, 3.0, 2.0,
            fill_color="#FF0000",
        )
        assert abs(result.width - 3.0) < 0.01
        assert result.fill_color == "#FF0000"

    def test_add_image(self):
        prs, slide = _make_slide_with_shapes([])
        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
            _create_minimal_png(f.name)
            try:
                result = add_image(slide, f.name, 1.0, 1.0, 3.0, 2.0)
                assert abs(result.left - 1.0) < 0.01
            finally:
                os.unlink(f.name)

    def test_add_element_invalid_type(self):
        prs, slide = _make_slide_with_shapes([])
        with pytest.raises(ValueError, match="Invalid shape_type"):
            add_shape(slide, "nonexistent", 1.0, 1.0, 2.0, 2.0)


# ---------------------------------------------------------------------------
# TestAlignElements
# ---------------------------------------------------------------------------


class TestAlignElements:
    def test_align_left(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 2, "height": 1},
            {"left": 5, "top": 2, "width": 2, "height": 1},
            {"left": 3, "top": 3, "width": 2, "height": 1},
        ])
        results = align_elements(
            slide,
            [ElementLocator(index=0), ElementLocator(index=1), ElementLocator(index=2)],
            AlignmentType.LEFT,
        )
        # All should share the leftmost edge
        lefts = [r.left for r in results]
        assert all(abs(l - lefts[0]) < 0.01 for l in lefts)

    def test_align_center(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 2, "height": 1},
            {"left": 5, "top": 2, "width": 4, "height": 1},
        ])
        results = align_elements(
            slide,
            [ElementLocator(index=0), ElementLocator(index=1)],
            AlignmentType.CENTER,
        )
        # Centers should match
        centers = [r.left + r.width / 2 for r in results]
        assert abs(centers[0] - centers[1]) < 0.05

    def test_align_right(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 2, "height": 1},
            {"left": 3, "top": 2, "width": 4, "height": 1},
        ])
        results = align_elements(
            slide,
            [ElementLocator(index=0), ElementLocator(index=1)],
            AlignmentType.RIGHT,
        )
        rights = [r.left + r.width for r in results]
        assert abs(rights[0] - rights[1]) < 0.05

    def test_align_distribute_h(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 0, "top": 1, "width": 1, "height": 1},
            {"left": 2, "top": 1, "width": 1, "height": 1},
            {"left": 6, "top": 1, "width": 1, "height": 1},
        ])
        results = align_elements(
            slide,
            [ElementLocator(index=0), ElementLocator(index=1), ElementLocator(index=2)],
            AlignmentType.DISTRIBUTE_H,
        )
        # Gaps between consecutive elements should be approximately equal
        gaps = []
        sorted_results = sorted(results, key=lambda r: r.left)
        for i in range(len(sorted_results) - 1):
            gap = sorted_results[i + 1].left - (sorted_results[i].left + sorted_results[i].width)
            gaps.append(gap)
        if len(gaps) >= 2:
            assert abs(gaps[0] - gaps[1]) < 0.1

    def test_align_too_few_elements(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 2, "height": 1},
        ])
        with pytest.raises(ValueError, match="at least 2"):
            align_elements(
                slide,
                [ElementLocator(index=0)],
                AlignmentType.LEFT,
            )


# ---------------------------------------------------------------------------
# TestGroupElements
# ---------------------------------------------------------------------------


class TestGroupElements:
    def test_group_basic(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 2, "height": 1, "text": "A"},
            {"left": 4, "top": 1, "width": 2, "height": 1, "text": "B"},
        ])
        result = group_elements(
            slide,
            [ElementLocator(index=0), ElementLocator(index=1)],
        )
        assert result.child_count == 2

    def test_group_too_few(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 2, "height": 1, "text": "Solo"},
        ])
        with pytest.raises(ValueError, match="at least 2"):
            group_elements(
                slide,
                [ElementLocator(index=0)],
            )


# ---------------------------------------------------------------------------
# TestElementLocator validation
# ---------------------------------------------------------------------------


class TestElementLocator:
    def test_locator_requires_at_least_one(self):
        with pytest.raises(ValueError, match="at least one"):
            ElementLocator()

    def test_locator_index_valid(self):
        loc = ElementLocator(index=0)
        assert loc.index == 0

    def test_locator_name_valid(self):
        loc = ElementLocator(name="Title 1")
        assert loc.name == "Title 1"

    def test_locator_text_match_valid(self):
        loc = ElementLocator(text_match="Revenue")
        assert loc.text_match == "Revenue"
