"""Tests for spatial validation rules in pptx_mcp/review/spatial.py."""

from __future__ import annotations

import json
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch

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

from pptx_mcp.brand.config import resolve_brand_config
from pptx_mcp.review.spatial import (
    SPATIAL_RULES,
    check_element_bounds,
    check_element_overlap,
    check_element_spacing,
    check_text_overflow,
)


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


def _brand():
    return resolve_brand_config()


def _make_slide_with_shapes(
    shape_specs: list[dict],
) -> tuple[Presentation, object]:
    """Create a prs with one blank slide containing shapes at exact positions.

    Each spec: {left, top, width, height} in inches, optional text, font_size.
    """
    prs = Presentation()
    slide = prs.slides.add_slide(prs.slide_layouts[6])  # blank

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

    return prs, slide


# ---------------------------------------------------------------------------
# TestOverlapDetection
# ---------------------------------------------------------------------------


class TestOverlapDetection:
    def test_overlapping_shapes_detected(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 2, "name": "Box A"},
            {"left": 2, "top": 1.5, "width": 3, "height": 2, "name": "Box B"},
        ])
        issues = check_element_overlap(slide, 0, _brand())
        assert len(issues) >= 1
        assert any("overlap" in i.message.lower() for i in issues)

    def test_non_overlapping_pass(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 2, "height": 1},
            {"left": 4, "top": 1, "width": 2, "height": 1},
        ])
        issues = check_element_overlap(slide, 0, _brand())
        assert len(issues) == 0

    def test_trivial_edge_touch_ignored(self):
        """Shapes touching exactly at an edge (within 0.05\" threshold) should not flag."""
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 2, "height": 1},
            {"left": 3.04, "top": 1, "width": 2, "height": 1},  # just touching
        ])
        issues = check_element_overlap(slide, 0, _brand())
        assert len(issues) == 0


# ---------------------------------------------------------------------------
# TestBoundsChecking
# ---------------------------------------------------------------------------


class TestBoundsChecking:
    def test_shape_extends_right(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 12, "top": 1, "width": 3, "height": 1},  # extends past 13.333\"
        ])
        issues = check_element_bounds(slide, 0, _brand(), slide_width=13.333, slide_height=7.5)
        assert len(issues) >= 1
        assert any("beyond slide" in i.message.lower() for i in issues)

    def test_shape_extends_bottom(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 7, "width": 2, "height": 1.5},  # extends past 7.5\"
        ])
        issues = check_element_bounds(slide, 0, _brand(), slide_width=13.333, slide_height=7.5)
        assert len(issues) >= 1

    def test_shape_within_bounds_pass(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 2},
        ])
        issues = check_element_bounds(slide, 0, _brand(), slide_width=13.333, slide_height=7.5)
        assert len(issues) == 0

    def test_negative_position_detected(self):
        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[6])
        txBox = slide.shapes.add_textbox(
            Inches(-0.5), Inches(1), Inches(2), Inches(1),
        )
        issues = check_element_bounds(slide, 0, _brand(), slide_width=13.333, slide_height=7.5)
        assert len(issues) >= 1
        assert any("left edge" in i.message.lower() for i in issues)


# ---------------------------------------------------------------------------
# TestSpacing
# ---------------------------------------------------------------------------


class TestSpacing:
    def test_close_spacing_warned(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 2, "height": 1},
            {"left": 3.05, "top": 1, "width": 2, "height": 1},  # 0.05\" gap
        ])
        issues = check_element_spacing(slide, 0, _brand(), min_gap=0.1)
        assert len(issues) >= 1
        assert any("spacing" in i.message.lower() or "tight" in i.message.lower() for i in issues)

    def test_adequate_spacing_pass(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 2, "height": 1},
            {"left": 3.5, "top": 1, "width": 2, "height": 1},  # 0.5\" gap
        ])
        issues = check_element_spacing(slide, 0, _brand(), min_gap=0.1)
        assert len(issues) == 0


# ---------------------------------------------------------------------------
# TestTextOverflow
# ---------------------------------------------------------------------------


class TestTextOverflow:
    def test_long_text_in_small_box(self):
        long_text = "This is a very long piece of text that should overflow " * 10
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 1, "height": 0.5, "text": long_text, "font_size": 14},
        ])
        issues = check_text_overflow(slide, 0, _brand())
        assert len(issues) >= 1
        assert any("overflow" in i.message.lower() for i in issues)

    def test_short_text_passes(self):
        prs, slide = _make_slide_with_shapes([
            {"left": 1, "top": 1, "width": 3, "height": 1, "text": "Hello"},
        ])
        issues = check_text_overflow(slide, 0, _brand())
        assert len(issues) == 0


# ---------------------------------------------------------------------------
# TestValidateSlideLayout (integration via server tool)
# ---------------------------------------------------------------------------


def _patch_and_import_server():
    """Import pptx_mcp.server with a patched FastMCP that accepts **kwargs."""
    from mcp.server.fastmcp import FastMCP

    _original_init = FastMCP.__init__

    def _flexible_init(self, *args, **kwargs):
        import inspect
        sig = inspect.signature(_original_init)
        valid_params = set(sig.parameters.keys())
        filtered = {k: v for k, v in kwargs.items() if k in valid_params}
        _original_init(self, *args, **filtered)

    with patch.object(FastMCP, "__init__", _flexible_init):
        mod_name = "pptx_mcp.server"
        cached = sys.modules.pop(mod_name, None)
        try:
            import pptx_mcp.server as server_mod
            return server_mod
        finally:
            pass


class TestValidateSlideLayout:
    def test_validate_slide_tool(self):
        """End-to-end: generate a PPTX and validate a slide."""
        from pptx_mcp.engine.generator import DeckGenerator
        from pptx_mcp.models.slides import (
            PresentationDefinition,
            PresentationMetadata,
            SlideDefinition,
            SlideType,
        )

        defn = PresentationDefinition(
            metadata=PresentationMetadata(title="Test"),
            slides=[
                SlideDefinition(
                    slide_type=SlideType.CONTENT,
                    title="Slide 1",
                    bullets=["Point A", "Point B"],
                ),
            ],
        )
        with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as f:
            gen = DeckGenerator()
            gen.generate(defn, f.name)

            server = _patch_and_import_server()
            result_json = server.validate_slide_layout(f.name, 0)
            result = json.loads(result_json)
            assert "issue_count" in result
            assert "issues" in result
            assert result["slide_index"] == 0

    def test_validate_clean_slide(self):
        """A simple slide with well-separated shapes should have few/no issues."""
        prs = Presentation()
        slide = prs.slides.add_slide(prs.slide_layouts[6])
        # Add two well-separated shapes
        slide.shapes.add_textbox(Inches(1), Inches(1), Inches(3), Inches(1))
        slide.shapes.add_textbox(Inches(7), Inches(4), Inches(3), Inches(1))

        with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as f:
            prs.save(f.name)
            server = _patch_and_import_server()
            result_json = server.validate_slide_layout(f.name, 0)
            result = json.loads(result_json)
            # Well-separated shapes should have zero overlap/bounds issues
            overlap_issues = [i for i in result["issues"] if "overlap" in i.get("message", "").lower()]
            assert len(overlap_issues) == 0
