"""Tests for Phase 3 gap filling — planner CARD_GRID, content mapper, GroupElement,
parser CARD_GRID, and review rules extensions."""

from __future__ import annotations

import tempfile
from pathlib import Path

import pytest

from pptx_mcp.agents.content_mapper import suggest_slide_type
from pptx_mcp.agents.planner import PlannerInput, SlidePlannerAgent
from pptx_mcp.brand.defaults import PRESETS, executive_default
from pptx_mcp.engine.generator import DeckGenerator
from pptx_mcp.engine.parser import parse_presentation
from pptx_mcp.models.slides import (
    CalloutBox,
    CardDefinition,
    ColumnContent,
    ConnectorElement,
    FunnelStage,
    AgendaItem,
    GroupElement,
    IconItem,
    KpiCard,
    Position,
    PresentationDefinition,
    PresentationMetadata,
    ProcessStep,
    ShapeElement,
    SlideDefinition,
    SlideType,
    SwotData,
    TeamMember,
    TextBox,
    Paragraph,
    TextRun,
)
from pptx_mcp.review.engine import ReviewEngine
from pptx_mcp.review.rules import (
    _gather_text,
    check_card_count,
    SLIDE_RULES,
)


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


def _make_definition(slides: list[SlideDefinition]) -> PresentationDefinition:
    return PresentationDefinition(
        metadata=PresentationMetadata(title="Gap Fill Test"),
        slides=slides,
    )


def _new_generator() -> DeckGenerator:
    return DeckGenerator(brand=executive_default())


@pytest.fixture
def brand():
    return executive_default()


# ---------------------------------------------------------------------------
# Step 1: Planner CARD_GRID
# ---------------------------------------------------------------------------


class TestPlannerCardGrid:
    def _skeleton(self, **kwargs) -> SlideDefinition:
        agent = SlidePlannerAgent()
        return agent._create_skeleton_slide(**kwargs)

    def test_planner_generates_card_grid(self) -> None:
        slide = self._skeleton(
            slide_type=SlideType.CARD_GRID,
            title="Our Services",
            index=1,
            input_data=PlannerInput(
                topic="Cloud Services",
                key_points=["AWS", "Azure", "GCP"],
            ),
        )
        assert slide.slide_type == SlideType.CARD_GRID
        assert len(slide.cards) == 3
        assert slide.cards[0].title == "AWS"

    def test_card_grid_has_body_and_icon(self) -> None:
        slide = self._skeleton(
            slide_type=SlideType.CARD_GRID,
            title="Services",
            index=0,
            input_data=PlannerInput(
                topic="Consulting",
                key_points=["Strategy", "Execution"],
            ),
        )
        for card in slide.cards:
            assert card.body is not None
            assert card.icon is not None

    def test_card_grid_default_without_key_points(self) -> None:
        slide = self._skeleton(
            slide_type=SlideType.CARD_GRID,
            title="Solutions",
            index=0,
            input_data=PlannerInput(topic="AI"),
        )
        assert len(slide.cards) == 4  # default 4 items
        assert "AI" in slide.cards[0].title

    def test_card_grid_has_speaker_notes(self) -> None:
        slide = self._skeleton(
            slide_type=SlideType.CARD_GRID,
            title="Products",
            index=0,
            input_data=PlannerInput(topic="SaaS"),
        )
        assert slide.speaker_notes


# ---------------------------------------------------------------------------
# Step 2: Content Mapper CARD_GRID
# ---------------------------------------------------------------------------


class TestContentMapperCardGrid:
    @pytest.mark.parametrize("title", [
        "Our Services",
        "Product Offerings",
        "Enterprise Solutions",
        "Consulting Packages",
        "Our Modules",
    ])
    def test_services_keywords_map_to_card_grid(self, title: str) -> None:
        assert suggest_slide_type(title) == SlideType.CARD_GRID

    def test_features_maps_to_icon_grid(self) -> None:
        # "features" should go to ICON_GRID, not CARD_GRID
        assert suggest_slide_type("Key Features") == SlideType.ICON_GRID

    def test_unknown_maps_to_content(self) -> None:
        assert suggest_slide_type("Untitled Blank Slide") == SlideType.CONTENT


# ---------------------------------------------------------------------------
# Step 3: GroupElement Rendering
# ---------------------------------------------------------------------------


class TestGroupElementRendering:
    def _render_with_group(self, group: GroupElement) -> Path:
        """Render a presentation with one slide containing a group element."""
        defn = _make_definition([
            SlideDefinition(
                slide_type=SlideType.CONTENT,
                title="Group Test",
                elements=[group],
            ),
        ])
        gen = _new_generator()
        tmp = tempfile.NamedTemporaryFile(suffix=".pptx", delete=False)
        gen.generate(defn, tmp.name)
        return Path(tmp.name)

    def test_group_with_two_shapes(self) -> None:
        group = GroupElement(
            elements=[
                ShapeElement(
                    shape_type="rectangle",
                    text="Box A",
                    position=Position(left=1.0, top=1.0, width=2.0, height=1.0),
                ),
                ShapeElement(
                    shape_type="rectangle",
                    text="Box B",
                    position=Position(left=4.0, top=1.0, width=2.0, height=1.0),
                ),
            ],
            position=Position(left=0.5, top=0.5, width=8.0, height=3.0),
        )
        path = self._render_with_group(group)
        assert path.exists()
        assert path.stat().st_size > 0

    def test_group_with_textbox(self) -> None:
        group = GroupElement(
            elements=[
                TextBox(
                    paragraphs=[Paragraph(runs=[TextRun(text="Hello")])],
                    position=Position(left=0.0, top=0.0, width=3.0, height=1.0),
                ),
            ],
            position=Position(left=1.0, top=2.0, width=5.0, height=3.0),
        )
        path = self._render_with_group(group)
        assert path.exists()

    def test_nested_group(self) -> None:
        inner = GroupElement(
            elements=[
                ShapeElement(
                    shape_type="rectangle",
                    text="Nested",
                    position=Position(left=0.0, top=0.0, width=1.0, height=1.0),
                ),
            ],
            position=Position(left=0.5, top=0.5, width=2.0, height=2.0),
        )
        outer = GroupElement(
            elements=[inner],
            position=Position(left=1.0, top=1.0, width=4.0, height=4.0),
        )
        path = self._render_with_group(outer)
        assert path.exists()

    def test_empty_group(self) -> None:
        group = GroupElement(elements=[], position=Position(left=0, top=0, width=1, height=1))
        path = self._render_with_group(group)
        assert path.exists()

    def test_group_without_position(self) -> None:
        group = GroupElement(
            elements=[
                ShapeElement(
                    shape_type="rectangle",
                    text="No offset",
                    position=Position(left=2.0, top=2.0, width=2.0, height=1.0),
                ),
            ],
        )
        path = self._render_with_group(group)
        assert path.exists()

    def test_connector_in_group_skipped(self) -> None:
        """Connectors inside groups should be skipped without error."""
        group = GroupElement(
            elements=[
                ShapeElement(
                    shape_type="rectangle",
                    text="Shape",
                    position=Position(left=0, top=0, width=2, height=1),
                ),
                ConnectorElement(
                    type="connector",
                    start_shape_index=0,
                    end_shape_index=1,
                ),
            ],
            position=Position(left=0, top=0, width=5, height=3),
        )
        path = self._render_with_group(group)
        assert path.exists()


# ---------------------------------------------------------------------------
# Step 4: Parser CARD_GRID Heuristic
# ---------------------------------------------------------------------------


class TestParserCardGrid:
    def test_card_grid_keyword_in_title_keywords(self) -> None:
        """CARD_GRID pattern exists in parser's _TITLE_KEYWORDS."""
        from pptx_mcp.engine.parser import _TITLE_KEYWORDS
        assert SlideType.CARD_GRID in _TITLE_KEYWORDS

    @pytest.mark.parametrize("title", [
        "Our Services",
        "Product Offerings",
        "Enterprise Solutions",
        "Consulting Packages",
    ])
    def test_card_grid_keyword_matches(self, title: str) -> None:
        """Parser keywords match expected service-related titles."""
        from pptx_mcp.engine.parser import _TITLE_KEYWORDS
        assert _TITLE_KEYWORDS[SlideType.CARD_GRID].search(title) is not None

    def test_keyword_classify_returns_card_grid(self) -> None:
        """_keyword_classify returns CARD_GRID for service titles."""
        from pptx_mcp.engine.parser import _keyword_classify
        result = _keyword_classify(
            title_text="Our Services Overview",
            total_text="Our Services Overview Strategy Implementation Analytics",
            total_non_footer_shapes=5,
            total_slides=5,
            slide_index=2,
        )
        assert result == SlideType.CARD_GRID


# ---------------------------------------------------------------------------
# Step 5: Review Rules — _gather_text + check_card_count
# ---------------------------------------------------------------------------


class TestGatherText:
    def test_includes_card_text(self) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.CARD_GRID,
            title="Cards",
            cards=[
                CardDefinition(title="Alpha", body="Body text", bullets=["Bullet one"]),
            ],
        )
        text = _gather_text(slide)
        assert "Alpha" in text
        assert "Body text" in text
        assert "Bullet one" in text

    def test_includes_kpi_text(self) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.DASHBOARD,
            title="KPIs",
            kpis=[KpiCard(value="99%", label="Uptime")],
        )
        text = _gather_text(slide)
        assert "99%" in text
        assert "Uptime" in text

    def test_includes_icon_text(self) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.ICON_GRID,
            title="Icons",
            icons=[IconItem(icon="\u2605", title="Speed", description="Fast delivery")],
        )
        text = _gather_text(slide)
        assert "Speed" in text
        assert "Fast delivery" in text

    def test_includes_team_text(self) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.TEAM,
            title="Team",
            team_members=[TeamMember(name="Alice", role="CTO", bio="Expert in AI")],
        )
        text = _gather_text(slide)
        assert "Alice" in text
        assert "CTO" in text
        assert "Expert in AI" in text

    def test_includes_callout_text(self) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Note",
            callout=CalloutBox(variant="info", title="Important", body="Check this"),
        )
        text = _gather_text(slide)
        assert "Important" in text
        assert "Check this" in text

    def test_includes_funnel_text(self) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.FUNNEL,
            title="Pipeline",
            funnel_stages=[FunnelStage(label="Awareness", description="Top of funnel")],
        )
        text = _gather_text(slide)
        assert "Awareness" in text
        assert "Top of funnel" in text

    def test_includes_agenda_text(self) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.AGENDA,
            title="Agenda",
            agenda_items=[AgendaItem(title="Intro", description="Welcome and overview")],
        )
        text = _gather_text(slide)
        assert "Intro" in text
        assert "Welcome and overview" in text

    def test_includes_swot_text(self) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.SWOT,
            title="SWOT",
            swot=SwotData(
                strengths=["Strong brand"],
                weaknesses=["Limited reach"],
                opportunities=["New markets"],
                threats=["Competition"],
            ),
        )
        text = _gather_text(slide)
        assert "Strong brand" in text
        assert "Limited reach" in text
        assert "New markets" in text
        assert "Competition" in text

    def test_includes_step_text(self) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.PROCESS,
            title="Process",
            steps=[ProcessStep(title="Plan", description="Create a roadmap")],
        )
        text = _gather_text(slide)
        assert "Plan" in text
        assert "Create a roadmap" in text


class TestCheckCardCount:
    def test_card_count_ok_at_6(self, brand) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.CARD_GRID,
            title="Services",
            cards=[CardDefinition(title=f"Card {i}") for i in range(6)],
        )
        issues = check_card_count(slide, 0, brand)
        assert len(issues) == 0

    def test_card_count_warns_at_7(self, brand) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.CARD_GRID,
            title="Services",
            cards=[CardDefinition(title=f"Card {i}") for i in range(7)],
        )
        issues = check_card_count(slide, 0, brand)
        assert len(issues) == 1
        assert "7 cards" in issues[0].message

    def test_kpi_count_ok_at_8(self, brand) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.DASHBOARD,
            title="Dashboard",
            kpis=[KpiCard(value=str(i), label=f"Metric {i}") for i in range(8)],
        )
        issues = check_card_count(slide, 0, brand)
        assert len(issues) == 0

    def test_kpi_count_warns_at_9(self, brand) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.DASHBOARD,
            title="Dashboard",
            kpis=[KpiCard(value=str(i), label=f"Metric {i}") for i in range(9)],
        )
        issues = check_card_count(slide, 0, brand)
        assert len(issues) == 1
        assert "9 KPIs" in issues[0].message

    def test_check_card_count_in_slide_rules(self) -> None:
        assert check_card_count in SLIDE_RULES
