"""Tests for layout variant engine and MCP tool integration."""

from __future__ import annotations

import json
from unittest.mock import patch

import pytest

from pptx_mcp.engine.variants import (
    VARIANT_GROUPS,
    _TYPE_TO_GROUP,
    adapt_slide,
    generate_hint,
    get_compatible_types,
)
from pptx_mcp.models.slides import (
    CardDefinition,
    ColumnContent,
    IconItem,
    KpiCard,
    PresentationDefinition,
    PresentationMetadata,
    ProcessStep,
    SlideDefinition,
    SlideType,
)


# ===================================================================
# TestVariantGroups — group structure integrity
# ===================================================================


class TestVariantGroups:
    def test_group_membership(self):
        """All types in variant groups are valid SlideTypes."""
        for group_name, types in VARIANT_GROUPS.items():
            for t in types:
                assert isinstance(t, SlideType), f"{t} in {group_name} is not a SlideType"

    def test_no_overlap(self):
        """No SlideType appears in multiple variant groups."""
        seen: dict[SlideType, str] = {}
        for group_name, types in VARIANT_GROUPS.items():
            for t in types:
                assert t not in seen, f"{t} in both {seen[t]} and {group_name}"
                seen[t] = group_name

    def test_reverse_lookup(self):
        """_TYPE_TO_GROUP correctly maps every grouped type."""
        for group_name, types in VARIANT_GROUPS.items():
            for t in types:
                assert _TYPE_TO_GROUP[t] == group_name

    def test_types_not_in_groups_return_empty(self):
        """Types not in any group have no compatible variants."""
        ungrouped = [
            SlideType.TITLE, SlideType.CLOSING, SlideType.TEAM,
            SlideType.SWOT, SlideType.FUNNEL, SlideType.AGENDA,
            SlideType.ORG_CHART, SlideType.CHART, SlideType.TABLE,
        ]
        for t in ungrouped:
            assert get_compatible_types(t) == [], f"{t} should have no variants"


# ===================================================================
# TestGetCompatibleTypes
# ===================================================================


class TestGetCompatibleTypes:
    def test_content_returns_3_variants(self):
        variants = get_compatible_types(SlideType.CONTENT)
        assert len(variants) == 3
        types = {v["slide_type"] for v in variants}
        assert types == {"two_column", "comparison", "pros_cons"}

    def test_process_returns_2_variants(self):
        variants = get_compatible_types(SlideType.PROCESS)
        assert len(variants) == 2
        types = {v["slide_type"] for v in variants}
        assert types == {"timeline", "step_cards"}

    def test_dashboard_returns_2_variants(self):
        variants = get_compatible_types(SlideType.DASHBOARD)
        assert len(variants) == 2
        types = {v["slide_type"] for v in variants}
        assert types == {"card_grid", "icon_grid"}

    def test_title_returns_empty(self):
        assert get_compatible_types(SlideType.TITLE) == []

    def test_all_grouped_types_callable(self):
        """Every type in a group returns non-empty list."""
        for types in VARIANT_GROUPS.values():
            for t in types:
                result = get_compatible_types(t)
                assert len(result) > 0, f"{t} should have variants"


# ===================================================================
# TestAdaptSlideGroupA — Structured Content
# ===================================================================


class TestAdaptSlideGroupA:
    def _content_slide(self, **kwargs) -> SlideDefinition:
        defaults = {
            "slide_type": SlideType.CONTENT,
            "title": "Test Slide",
            "bullets": ["Alpha", "Beta", "Gamma", "Delta", "Echo", "Foxtrot"],
            "speaker_notes": "These are notes",
        }
        defaults.update(kwargs)
        return SlideDefinition(**defaults)

    def test_content_to_two_column(self):
        src = self._content_slide()
        result = adapt_slide(src, SlideType.TWO_COLUMN)
        assert result is not None
        assert result.slide_type == SlideType.TWO_COLUMN
        assert len(result.columns) == 2
        assert len(result.columns[0].bullets) == 3
        assert len(result.columns[1].bullets) == 3
        assert result.columns[0].bullets[0] == "Alpha"
        assert result.columns[1].bullets[0] == "Delta"

    def test_content_to_comparison(self):
        src = self._content_slide()
        result = adapt_slide(src, SlideType.COMPARISON)
        assert result is not None
        assert result.slide_type == SlideType.COMPARISON
        assert len(result.columns) == 2

    def test_content_to_pros_cons(self):
        src = self._content_slide()
        result = adapt_slide(src, SlideType.PROS_CONS)
        assert result is not None
        assert result.slide_type == SlideType.PROS_CONS
        assert len(result.pros) == 3
        assert len(result.cons) == 3
        assert result.pros[0] == "Alpha"
        assert result.cons[0] == "Delta"

    def test_two_column_to_content(self):
        src = SlideDefinition(
            slide_type=SlideType.TWO_COLUMN,
            title="Two Col",
            columns=[
                ColumnContent(title="Left", bullets=["A", "B"]),
                ColumnContent(title="Right", bullets=["C", "D"]),
            ],
        )
        result = adapt_slide(src, SlideType.CONTENT)
        assert result is not None
        assert result.slide_type == SlideType.CONTENT
        assert result.bullets == ["A", "B", "C", "D"]

    def test_two_column_to_pros_cons(self):
        src = SlideDefinition(
            slide_type=SlideType.TWO_COLUMN,
            title="Two Col",
            columns=[
                ColumnContent(title="Good", bullets=["A", "B"]),
                ColumnContent(title="Bad", bullets=["C", "D"]),
            ],
        )
        result = adapt_slide(src, SlideType.PROS_CONS)
        assert result is not None
        assert result.pros == ["A", "B"]
        assert result.cons == ["C", "D"]

    def test_pros_cons_to_content(self):
        src = SlideDefinition(
            slide_type=SlideType.PROS_CONS,
            title="ProsCons",
            pros=["Good1", "Good2"],
            cons=["Bad1", "Bad2"],
        )
        result = adapt_slide(src, SlideType.CONTENT)
        assert result is not None
        assert result.bullets == ["Good1", "Good2", "Bad1", "Bad2"]

    def test_pros_cons_to_two_column(self):
        src = SlideDefinition(
            slide_type=SlideType.PROS_CONS,
            title="ProsCons",
            pros=["Good1", "Good2"],
            cons=["Bad1"],
        )
        result = adapt_slide(src, SlideType.TWO_COLUMN)
        assert result is not None
        assert result.columns[0].title == "Pros"
        assert result.columns[0].bullets == ["Good1", "Good2"]
        assert result.columns[1].title == "Cons"
        assert result.columns[1].bullets == ["Bad1"]

    def test_preserves_common_fields(self):
        src = self._content_slide(
            subtitle="Sub",
            background_color="#FF0000",
        )
        result = adapt_slide(src, SlideType.TWO_COLUMN)
        assert result is not None
        assert result.title == "Test Slide"
        assert result.subtitle == "Sub"
        assert result.speaker_notes == "These are notes"
        assert result.background_color == "#FF0000"


# ===================================================================
# TestAdaptSlideGroupB — Sequential Steps
# ===================================================================


class TestAdaptSlideGroupB:
    def _process_slide(self) -> SlideDefinition:
        return SlideDefinition(
            slide_type=SlideType.PROCESS,
            title="Process Flow",
            steps=[
                ProcessStep(title="Plan", description="Planning phase"),
                ProcessStep(title="Build", description="Build phase"),
                ProcessStep(title="Deploy", description="Deploy phase"),
            ],
            active_step=1,
        )

    def test_process_to_timeline_adds_dates(self):
        src = self._process_slide()
        result = adapt_slide(src, SlideType.TIMELINE)
        assert result is not None
        assert result.slide_type == SlideType.TIMELINE
        assert len(result.steps) == 3
        # Dates populated since source had none
        assert result.steps[0].date == "Q1"
        assert result.steps[1].date == "Q2"
        assert result.steps[2].date == "Q3"

    def test_process_to_step_cards(self):
        src = self._process_slide()
        result = adapt_slide(src, SlideType.STEP_CARDS)
        assert result is not None
        assert result.slide_type == SlideType.STEP_CARDS
        assert len(result.steps) == 3
        assert result.steps[0].title == "Plan"

    def test_timeline_to_process(self):
        src = SlideDefinition(
            slide_type=SlideType.TIMELINE,
            title="Timeline",
            steps=[
                ProcessStep(title="Phase 1", date="Jan"),
                ProcessStep(title="Phase 2", date="Feb"),
            ],
        )
        result = adapt_slide(src, SlideType.PROCESS)
        assert result is not None
        assert result.slide_type == SlideType.PROCESS
        assert len(result.steps) == 2

    def test_preserves_step_count(self):
        src = self._process_slide()
        for target in [SlideType.TIMELINE, SlideType.STEP_CARDS]:
            result = adapt_slide(src, target)
            assert result is not None
            assert len(result.steps) == len(src.steps)


# ===================================================================
# TestAdaptSlideGroupC — Grids
# ===================================================================


class TestAdaptSlideGroupC:
    def test_dashboard_to_card_grid(self):
        src = SlideDefinition(
            slide_type=SlideType.DASHBOARD,
            title="Metrics",
            kpis=[
                KpiCard(value="$1.2M", label="Revenue"),
                KpiCard(value="95%", label="Uptime"),
            ],
        )
        result = adapt_slide(src, SlideType.CARD_GRID)
        assert result is not None
        assert result.slide_type == SlideType.CARD_GRID
        assert len(result.cards) == 2
        assert result.cards[0].title == "Revenue"
        assert result.cards[0].body == "$1.2M"

    def test_dashboard_to_icon_grid(self):
        src = SlideDefinition(
            slide_type=SlideType.DASHBOARD,
            title="Metrics",
            kpis=[
                KpiCard(value="$1.2M", label="Revenue"),
            ],
        )
        result = adapt_slide(src, SlideType.ICON_GRID)
        assert result is not None
        assert result.slide_type == SlideType.ICON_GRID
        assert len(result.icons) == 1
        assert result.icons[0].title == "Revenue"
        assert result.icons[0].description == "$1.2M"

    def test_card_grid_to_icon_grid(self):
        src = SlideDefinition(
            slide_type=SlideType.CARD_GRID,
            title="Features",
            cards=[
                CardDefinition(title="Speed", body="Fast delivery", icon="\u26a1"),
                CardDefinition(title="Scale", body="Grows with you"),
            ],
        )
        result = adapt_slide(src, SlideType.ICON_GRID)
        assert result is not None
        assert len(result.icons) == 2
        assert result.icons[0].icon == "\u26a1"
        assert result.icons[0].title == "Speed"
        assert result.icons[1].icon == "\u2b24"  # default fallback

    def test_icon_grid_to_card_grid(self):
        src = SlideDefinition(
            slide_type=SlideType.ICON_GRID,
            title="Features",
            icons=[
                IconItem(icon="\u2764", title="Love", description="We love code"),
            ],
        )
        result = adapt_slide(src, SlideType.CARD_GRID)
        assert result is not None
        assert len(result.cards) == 1
        assert result.cards[0].title == "Love"
        assert result.cards[0].icon == "\u2764"
        assert result.cards[0].body == "We love code"


# ===================================================================
# TestAdaptSlideIncompatible
# ===================================================================


class TestAdaptSlideIncompatible:
    def test_cross_group_returns_none(self):
        """CONTENT (structured) → PROCESS (sequential) should return None."""
        src = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Test",
            bullets=["A", "B"],
        )
        assert adapt_slide(src, SlideType.PROCESS) is None

    def test_title_to_anything_returns_none(self):
        src = SlideDefinition(slide_type=SlideType.TITLE, title="Title")
        assert adapt_slide(src, SlideType.CONTENT) is None
        assert adapt_slide(src, SlideType.DASHBOARD) is None

    def test_diagram_to_anything_returns_none(self):
        src = SlideDefinition(slide_type=SlideType.ORG_CHART, title="Org")
        assert adapt_slide(src, SlideType.CONTENT) is None
        assert adapt_slide(src, SlideType.PROCESS) is None


# ===================================================================
# TestGenerateHint
# ===================================================================


class TestGenerateHint:
    def test_structured_hint(self):
        src = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Test",
            bullets=["A", "B", "C", "D"],
        )
        hint = generate_hint(src, SlideType.TWO_COLUMN)
        assert "4 bullets" in hint
        assert "2 columns" in hint

    def test_sequential_hint(self):
        src = SlideDefinition(
            slide_type=SlideType.PROCESS,
            title="Test",
            steps=[ProcessStep(title="S1"), ProcessStep(title="S2")],
        )
        hint = generate_hint(src, SlideType.TIMELINE)
        assert "2 steps" in hint

    def test_grid_hint(self):
        src = SlideDefinition(
            slide_type=SlideType.DASHBOARD,
            title="Test",
            kpis=[KpiCard(value="1", label="A"), KpiCard(value="2", label="B")],
        )
        hint = generate_hint(src, SlideType.CARD_GRID)
        assert "2 KPIs" in hint
        assert "2 cards" in hint

    def test_incompatible_hint(self):
        src = SlideDefinition(slide_type=SlideType.TITLE, title="Title")
        hint = generate_hint(src, SlideType.CONTENT)
        assert "re-generated" in hint


# ===================================================================
# TestGetLayoutVariantsTool — server integration
# ===================================================================


class TestGetLayoutVariantsTool:
    """Tests for the get_layout_variants MCP tool."""

    def _setup_plan(self, slides: list[SlideDefinition]) -> str:
        """Cache a plan and return the plan_id."""
        from pptx_mcp.server import _cache_plan

        defn = PresentationDefinition(
            metadata=PresentationMetadata(title="Test Deck"),
            slides=slides,
        )
        entry = _cache_plan(defn, topic="Test")
        return entry.plan_id

    def test_returns_variants_for_content(self):
        from pptx_mcp.server import get_layout_variants

        plan_id = self._setup_plan([
            SlideDefinition(
                slide_type=SlideType.CONTENT,
                title="Content Slide",
                bullets=["A", "B", "C", "D"],
            ),
        ])
        result = json.loads(get_layout_variants(plan_id, 0))
        assert result["current_type"] == "content"
        assert result["current_title"] == "Content Slide"
        assert len(result["variants"]) == 3
        types = {v["slide_type"] for v in result["variants"]}
        assert "two_column" in types
        # Hints should be populated
        for v in result["variants"]:
            assert "hint" in v
            assert len(v["hint"]) > 0

    def test_returns_empty_for_title(self):
        from pptx_mcp.server import get_layout_variants

        plan_id = self._setup_plan([
            SlideDefinition(slide_type=SlideType.TITLE, title="Title"),
        ])
        result = json.loads(get_layout_variants(plan_id, 0))
        assert result["variants"] == []

    def test_invalid_plan_id(self):
        from pptx_mcp.server import get_layout_variants
        from mcp.server.fastmcp.exceptions import ToolError

        with pytest.raises(ToolError, match="not found"):
            get_layout_variants("nonexistent_id", 0)

    def test_invalid_slide_index(self):
        from pptx_mcp.server import get_layout_variants
        from mcp.server.fastmcp.exceptions import ToolError

        plan_id = self._setup_plan([
            SlideDefinition(slide_type=SlideType.CONTENT, title="Slide"),
        ])
        with pytest.raises(ToolError):
            get_layout_variants(plan_id, 5)


# ===================================================================
# TestUpdatePlanSlideAdapter — content-preserving type changes
# ===================================================================


class TestUpdatePlanSlideAdapter:
    """Tests for the enhanced update_plan_slide with variant adaptation."""

    def _setup_plan(self, slides: list[SlideDefinition], topic: str = "Test") -> str:
        from pptx_mcp.server import _cache_plan

        defn = PresentationDefinition(metadata=PresentationMetadata(title="Deck"), slides=slides)
        entry = _cache_plan(defn, topic=topic)
        return entry.plan_id

    def test_within_group_preserves_content(self):
        """Changing CONTENT → TWO_COLUMN should preserve bullets."""
        from pptx_mcp.server import update_plan_slide, _get_cached_plan

        plan_id = self._setup_plan([
            SlideDefinition(
                slide_type=SlideType.CONTENT,
                title="My Slide",
                bullets=["A", "B", "C", "D"],
            ),
        ])
        result = json.loads(update_plan_slide(plan_id, 0, slide_type="two_column"))
        assert result["status"] == "success"
        assert result["slide_type"] == "two_column"
        assert result["mode"] == "type_change"

        # Verify content was preserved
        entry = _get_cached_plan(plan_id)
        slide = entry.definition.slides[0]
        assert slide.slide_type == SlideType.TWO_COLUMN
        assert len(slide.columns) == 2
        all_bullets = []
        for col in slide.columns:
            all_bullets.extend(col.bullets)
        assert all_bullets == ["A", "B", "C", "D"]

    def test_cross_group_rehydrates(self):
        """Changing CONTENT → PROCESS should fall back to skeleton."""
        from pptx_mcp.server import update_plan_slide, _get_cached_plan

        plan_id = self._setup_plan([
            SlideDefinition(
                slide_type=SlideType.CONTENT,
                title="My Slide",
                bullets=["A", "B"],
            ),
        ], topic="Project Plan")
        result = json.loads(update_plan_slide(plan_id, 0, slide_type="process"))
        assert result["status"] == "success"
        assert result["slide_type"] == "process"

        entry = _get_cached_plan(plan_id)
        slide = entry.definition.slides[0]
        assert slide.slide_type == SlideType.PROCESS
        # Should have steps from skeleton, not adapted bullets
        assert len(slide.steps) > 0

    def test_backward_compat_text_overrides(self):
        """Text-only updates still work as before."""
        from pptx_mcp.server import update_plan_slide, _get_cached_plan

        plan_id = self._setup_plan([
            SlideDefinition(
                slide_type=SlideType.CONTENT,
                title="Original",
                bullets=["X"],
            ),
        ])
        result = json.loads(update_plan_slide(plan_id, 0, title="Updated Title"))
        assert result["status"] == "success"
        assert result["title"] == "Updated Title"

        entry = _get_cached_plan(plan_id)
        assert entry.definition.slides[0].title == "Updated Title"
        assert entry.definition.slides[0].bullets == ["X"]


# ===================================================================
# TestEdgeCases — additional edge cases from review
# ===================================================================


class TestEdgeCases:
    def test_adapt_same_type_returns_copy(self):
        """Adapting to the same type should return a copy."""
        src = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Same",
            bullets=["A"],
        )
        result = adapt_slide(src, SlideType.CONTENT)
        assert result is not None
        assert result.slide_type == SlideType.CONTENT
        assert result.bullets == ["A"]
        assert result is not src

    def test_adapt_with_empty_bullets(self):
        """Adapting a CONTENT slide with no bullets should produce empty columns."""
        src = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Empty",
            bullets=[],
        )
        result = adapt_slide(src, SlideType.TWO_COLUMN)
        assert result is not None
        assert len(result.columns) == 2
        assert result.columns[0].bullets == []
        assert result.columns[1].bullets == []

    def test_adapt_with_empty_steps(self):
        """Adapting a PROCESS slide with no steps."""
        src = SlideDefinition(
            slide_type=SlideType.PROCESS,
            title="Empty Process",
            steps=[],
        )
        result = adapt_slide(src, SlideType.TIMELINE)
        assert result is not None
        assert result.steps == []

    def test_comparison_3_cols_to_two_column(self):
        """COMPARISON with 3 columns → TWO_COLUMN should take first 2."""
        src = SlideDefinition(
            slide_type=SlideType.COMPARISON,
            title="3 Cols",
            columns=[
                ColumnContent(title="A", bullets=["a1"]),
                ColumnContent(title="B", bullets=["b1"]),
                ColumnContent(title="C", bullets=["c1"]),
            ],
        )
        result = adapt_slide(src, SlideType.TWO_COLUMN)
        assert result is not None
        assert len(result.columns) == 2
        assert result.columns[0].title == "A"
        assert result.columns[1].title == "B"

    def test_split_into_columns_more_cols_than_items(self):
        """Splitting 2 bullets into 4 columns should produce some empty groups."""
        from pptx_mcp.engine.variants import _split_into_columns
        result = _split_into_columns(["A", "B"], 4)
        assert len(result) == 4
        # At least some columns should have content
        total = sum(len(g) for g in result)
        assert total == 2

    def test_preserves_title_style(self):
        """Adaptation should preserve title_style and subtitle_style."""
        from pptx_mcp.models.slides import TextStyle
        style = TextStyle(font_size=36, bold=True)
        src = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Styled",
            title_style=style,
            bullets=["A"],
        )
        result = adapt_slide(src, SlideType.TWO_COLUMN)
        assert result is not None
        assert result.title_style is not None
        assert result.title_style.font_size == 36

    def test_hint_with_empty_steps(self):
        """Hint should handle empty steps without error."""
        src = SlideDefinition(
            slide_type=SlideType.PROCESS,
            title="Empty",
            steps=[],
        )
        hint = generate_hint(src, SlideType.TIMELINE)
        assert "0 steps" in hint


# ===================================================================
# TestKeyPointTruncation — planner truncation helper
# ===================================================================


class TestKeyPointTruncation:
    def test_truncate_short_text(self):
        from pptx_mcp.agents.planner import _truncate
        assert _truncate("short") == "short"

    def test_truncate_long_text(self):
        from pptx_mcp.agents.planner import _truncate
        long_text = "A" * 150
        result = _truncate(long_text)
        assert len(result) <= 97
        assert result.endswith("\u2026")

    def test_truncate_custom_max(self):
        from pptx_mcp.agents.planner import _truncate
        result = _truncate("Hello World", 5)
        assert len(result) == 5
        assert result.endswith("\u2026")

    def test_planner_handles_long_key_points(self):
        """Planner should not crash on key points > 100 chars."""
        from pptx_mcp.agents.planner import PlannerInput, SlidePlannerAgent
        long_kp = "GlobalMed's supply chain has 23 distribution centers across 14 countries with $4.8B in annual logistics spend"
        assert len(long_kp) > 100
        planner = SlidePlannerAgent()
        slide = planner._create_skeleton_slide(
            SlideType.PROCESS,
            "Test",
            0,
            PlannerInput(topic="Test", key_points=[long_kp]),
        )
        assert slide.slide_type == SlideType.PROCESS
        assert len(slide.steps) > 0
        for step in slide.steps:
            assert len(step.title) <= 100
