"""Tests for MCP server tool handlers in pptx_mcp/server.py.

Each tool function returns a JSON string; we parse and validate the results.

Note: We patch FastMCP.__init__ before importing the server module to handle
kwarg compatibility across different mcp library versions.
"""

from __future__ import annotations

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

import pytest

from pptx_mcp.brand.defaults import executive_default
from pptx_mcp.engine.generator import DeckGenerator
from pptx_mcp.models.slides import (
    PresentationDefinition,
    PresentationMetadata,
    SlideDefinition,
    SlideType,
)


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):
        # Strip unknown kwargs so older/newer mcp versions both work
        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):
        # Remove cached module if present so the patch applies during import
        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  # leave the newly-imported module in sys.modules


server = _patch_and_import_server()

add_speaker_notes = server.add_speaker_notes
create_from_plan = server.create_from_plan
create_presentation = server.create_presentation
delete_slide = server.delete_slide
duplicate_slide = server.duplicate_slide
generate_presentation = server.generate_presentation
get_brand_presets = server.get_brand_presets
get_deck_types = server.get_deck_types
get_plan_slide = server.get_plan_slide
get_slide_templates = server.get_slide_templates
move_slide = server.move_slide
optimize_plan = server.optimize_plan
plan_presentation = server.plan_presentation
read_presentation = server.read_presentation
restyle_presentation = server.restyle_presentation
review_presentation = server.review_presentation

# Import ToolError for error assertion tests
from mcp.server.fastmcp.exceptions import ToolError


def _sample_definition() -> PresentationDefinition:
    """Minimal valid PresentationDefinition for testing."""
    return PresentationDefinition(
        metadata=PresentationMetadata(title="Server Test Deck"),
        slides=[
            SlideDefinition(slide_type=SlideType.TITLE, title="Hello", subtitle="World"),
            SlideDefinition(slide_type=SlideType.CONTENT, title="Points", bullets=["A", "B"]),
        ],
    )


def _generate_sample_pptx(tmp_path: Path, num_slides: int = 3) -> Path:
    """Generate a real pptx file for tests that need to read an existing deck."""
    slides = [
        SlideDefinition(slide_type=SlideType.TITLE, title="Slide 1", subtitle="Sub"),
    ]
    for i in range(2, num_slides + 1):
        slides.append(
            SlideDefinition(
                slide_type=SlideType.CONTENT,
                title=f"Slide {i}",
                bullets=[f"Bullet {i}.1", f"Bullet {i}.2"],
            )
        )
    definition = PresentationDefinition(
        metadata=PresentationMetadata(title="Sample Deck"),
        slides=slides,
    )
    generator = DeckGenerator(brand=executive_default())
    out = tmp_path / "sample.pptx"
    generator.generate(definition, out)
    return out


class TestCreatePresentation:
    """Tests for the create_presentation tool."""

    def test_valid_definition(self, tmp_path: Path) -> None:
        out = str(tmp_path / "out.pptx")
        result = json.loads(create_presentation(_sample_definition(), out))
        assert result["status"] == "success"
        assert result["slide_count"] == 2
        assert Path(result["output_path"]).exists()

    def test_pydantic_rejects_missing_metadata(self) -> None:
        """PresentationDefinition requires metadata — Pydantic catches this."""
        from pydantic import ValidationError

        with pytest.raises(ValidationError):
            PresentationDefinition.model_validate({"slides": []})

    def test_with_brand_preset(self, tmp_path: Path) -> None:
        out = str(tmp_path / "branded.pptx")
        result = json.loads(
            create_presentation(_sample_definition(), out, brand_preset="technical")
        )
        assert result["status"] == "success"


class TestPlanPresentation:
    """Tests for the plan_presentation tool."""

    def test_basic_plan(self) -> None:
        result_json = plan_presentation(topic="Test Topic", num_slides=5)
        result = json.loads(result_json)
        assert "slides" in result
        assert "plan_id" in result
        assert len(result["slides"]) >= 3

    def test_plan_with_key_points(self) -> None:
        result_json = plan_presentation(
            topic="Strategy",
            num_slides=8,
            key_points=["Cost", "Speed", "Quality"],
        )
        result = json.loads(result_json)
        assert "slides" in result
        assert "plan_id" in result
        assert len(result["slides"]) >= 3

    def test_plan_returns_title_slide_first(self) -> None:
        result_json = plan_presentation(topic="First Slide Check", num_slides=5)
        result = json.loads(result_json)
        assert result["slides"][0]["slide_type"] == "title"

    def test_plan_full_json_backward_compat(self) -> None:
        result_json = plan_presentation(topic="Full JSON", num_slides=5, return_full_json=True)
        result = json.loads(result_json)
        # Full JSON has metadata key, not plan_id
        assert "metadata" in result
        assert "plan_id" not in result


class TestGetSlideTemplates:
    """Tests for the get_slide_templates tool."""

    def test_returns_all_slide_types(self) -> None:
        result = json.loads(get_slide_templates())
        expected_types = {
            "title", "section", "content", "two_column", "pros_cons",
            "chart", "comparison", "diagram", "table", "blank",
            "quote", "big_number", "process", "timeline", "step_cards",
            "dashboard", "image_text", "icon_grid", "card_grid",
            "team", "closing", "swot", "funnel", "agenda",
            "org_chart", "matrix", "pyramid", "venn", "waterfall",
            "hub_spoke", "cycle", "gantt",
        }
        assert set(result.keys()) == expected_types

    def test_each_template_has_description(self) -> None:
        result = json.loads(get_slide_templates())
        for name, template in result.items():
            assert "description" in template, f"Missing description for {name}"

    def test_each_template_has_fields(self) -> None:
        result = json.loads(get_slide_templates())
        for name, template in result.items():
            assert "fields" in template, f"Missing fields for {name}"


class TestGetBrandPresets:
    """Tests for the get_brand_presets tool."""

    def test_returns_all_presets(self) -> None:
        result = json.loads(get_brand_presets())
        assert set(result.keys()) == {
            "executive", "technical", "minimal", "creative", "corporate", "consulting",
            "healthcare", "startup", "academic", "government", "finance", "nonprofit",
        }

    def test_presets_have_required_keys(self) -> None:
        result = json.loads(get_brand_presets())
        expected_keys = {
            "name", "tone", "primary_color", "secondary_color",
            "accent_color", "heading_font", "body_font",
        }
        for preset_name, preset in result.items():
            assert set(preset.keys()) == expected_keys, (
                f"Preset '{preset_name}' has unexpected keys"
            )


class TestReadPresentation:
    """Tests for the read_presentation tool."""

    def test_read_generated_pptx(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=2)
        result_json = read_presentation(str(pptx_path))
        result = json.loads(result_json)
        assert "slides" in result
        assert len(result["slides"]) == 2

    def test_read_nonexistent_file(self) -> None:
        with pytest.raises(ToolError):
            read_presentation("/nonexistent/path/deck.pptx")


class TestDeleteSlide:
    """Tests for the delete_slide tool."""

    def test_delete_middle_slide_succeeds(self, tmp_path: Path) -> None:
        """delete_slide removes a slide from the middle of the deck."""
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=3)
        result = json.loads(delete_slide(str(pptx_path), slide_index=1))
        assert result["status"] == "success"

    def test_delete_invalid_index(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=2)
        with pytest.raises(ToolError):
            delete_slide(str(pptx_path), slide_index=99)

    def test_delete_nonexistent_file(self) -> None:
        with pytest.raises(ToolError):
            delete_slide("/nonexistent/deck.pptx", slide_index=0)


class TestReviewPresentation:
    """Tests for the review_presentation tool."""

    def test_review_returns_score(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=3)
        result_json = review_presentation(str(pptx_path))
        result = json.loads(result_json)
        assert "overall_score" in result
        assert 0.0 <= result["overall_score"] <= 10.0

    def test_review_returns_issues(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=2)
        result_json = review_presentation(str(pptx_path))
        result = json.loads(result_json)
        assert "issues" in result
        assert isinstance(result["issues"], list)

    def test_review_with_preset(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=2)
        result_json = review_presentation(str(pptx_path), brand_preset="minimal")
        result = json.loads(result_json)
        assert "overall_score" in result


class TestMoveSlide:
    """Tests for the move_slide tool."""

    def test_move_slide_forward(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=3)
        result = json.loads(move_slide(str(pptx_path), from_index=0, to_index=2))
        assert result["status"] == "success"
        assert result["moved"]["from"] == 0
        assert result["moved"]["to"] == 2

    def test_move_slide_invalid_index(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=2)
        with pytest.raises(ToolError):
            move_slide(str(pptx_path), from_index=0, to_index=99)

    def test_move_slide_nonexistent_file(self) -> None:
        with pytest.raises(ToolError):
            move_slide("/nonexistent/deck.pptx", from_index=0, to_index=1)


class TestDuplicateSlide:
    """Tests for the duplicate_slide tool."""

    def test_duplicate_first_slide(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=2)
        result = json.loads(duplicate_slide(str(pptx_path), slide_index=0))
        assert result["status"] == "success"
        assert result["new_slide_index"] == 1

    def test_duplicate_invalid_index(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=2)
        with pytest.raises(ToolError):
            duplicate_slide(str(pptx_path), slide_index=99)


class TestGeneratePresentation:
    """Tests for the generate_presentation composite tool."""

    def test_generate_basic(self, tmp_path: Path) -> None:
        out = str(tmp_path / "gen.pptx")
        result = json.loads(generate_presentation(topic="Test Topic", output_path=out, num_slides=5))
        assert result["status"] == "success"
        assert Path(result["output_path"]).exists()
        assert result["slide_count"] >= 3

    def test_generate_with_key_points(self, tmp_path: Path) -> None:
        out = str(tmp_path / "gen_kp.pptx")
        result = json.loads(generate_presentation(
            topic="Strategy",
            output_path=out,
            num_slides=6,
            key_points=["Cost", "Speed", "Quality"],
        ))
        assert result["status"] == "success"
        assert Path(result["output_path"]).exists()


class TestGetDeckTypes:
    """Tests for the get_deck_types tool."""

    def test_returns_all_archetypes(self) -> None:
        result = json.loads(get_deck_types())
        expected = {
            "pitch_deck", "board_deck", "sales_deck", "all_hands",
            "project_update", "training", "strategy_deck", "due_diligence",
            "market_analysis", "org_design", "competitive_assessment",
            "transformation_roadmap",
            # v0.5.0
            "assessment", "opportunity_assessment", "strategy_training",
            "template_guide", "executive_overview", "analysis_deck",
            "cost_breakdown", "architecture_review", "analytics_dashboard",
            "product_launch", "case_study", "quarterly_earnings",
            "onboarding", "annual_report", "conference_talk",
        }
        assert set(result.keys()) == expected

    def test_archetypes_have_required_fields(self) -> None:
        result = json.loads(get_deck_types())
        for name, arch in result.items():
            assert "display_name" in arch, f"Missing display_name for {name}"
            assert "description" in arch, f"Missing description for {name}"
            assert "min_slides" in arch, f"Missing min_slides for {name}"
            assert "max_slides" in arch, f"Missing max_slides for {name}"
            assert "slot_sequence" in arch, f"Missing slot_sequence for {name}"
            assert len(arch["slot_sequence"]) >= 3, f"Too few slots for {name}"

    def test_archetypes_start_with_title(self) -> None:
        result = json.loads(get_deck_types())
        for name, arch in result.items():
            first_slot = arch["slot_sequence"][0]
            assert first_slot["slide_type"] == "title", (
                f"Archetype '{name}' should start with title slide"
            )


class TestGenerateWithDeckType:
    """Tests for deck_type parameter in generate_presentation."""

    def test_generate_with_pitch_deck(self, tmp_path: Path) -> None:
        out = str(tmp_path / "pitch.pptx")
        result = json.loads(generate_presentation(
            topic="AI Startup",
            output_path=out,
            num_slides=10,
            deck_type="pitch_deck",
        ))
        assert result["status"] == "success"
        assert Path(result["output_path"]).exists()
        assert result["slide_count"] >= 5

    def test_plan_with_deck_type(self) -> None:
        result_json = plan_presentation(
            topic="Board Update Q4",
            num_slides=10,
            deck_type="board_deck",
        )
        result = json.loads(result_json)
        types = [s["slide_type"] for s in result["slides"]]
        assert types[0] == "title"
        # Board deck should have diverse types
        unique_types = set(types)
        assert len(unique_types) >= 4


class TestPlanCache:
    """Tests for plan cache workflow: plan -> create_from_plan / get_plan_slide."""

    def test_plan_returns_plan_id(self) -> None:
        result = json.loads(plan_presentation(topic="Cache Test", num_slides=5))
        assert "plan_id" in result
        assert len(result["plan_id"]) == 12
        # Compact summary should NOT have metadata key
        assert "metadata" not in result

    def test_plan_full_json_mode(self) -> None:
        result = json.loads(plan_presentation(topic="Full Mode", num_slides=5, return_full_json=True))
        assert "metadata" in result
        assert "plan_id" not in result

    def test_create_from_plan_basic(self, tmp_path: Path) -> None:
        plan = json.loads(plan_presentation(topic="Render Test", num_slides=5))
        plan_id = plan["plan_id"]
        out = str(tmp_path / "from_plan.pptx")
        result = json.loads(create_from_plan(plan_id, out))
        assert result["status"] == "success"
        assert Path(result["output_path"]).exists()
        assert result["slide_count"] >= 3

    def test_create_from_plan_with_brand(self, tmp_path: Path) -> None:
        plan = json.loads(plan_presentation(topic="Brand Test", num_slides=5))
        plan_id = plan["plan_id"]
        out = str(tmp_path / "branded.pptx")
        result = json.loads(create_from_plan(plan_id, out, brand_preset="consulting"))
        assert result["status"] == "success"
        assert Path(result["output_path"]).exists()

    def test_create_from_plan_invalid_id(self, tmp_path: Path) -> None:
        out = str(tmp_path / "nope.pptx")
        with pytest.raises(ToolError, match="not found"):
            create_from_plan("nonexistent123", out)

    def test_plan_summary_fields(self) -> None:
        result = json.loads(plan_presentation(
            topic="Dashboard Demo",
            num_slides=8,
            key_points=["Revenue growth", "Cost reduction"],
        ))
        assert "slide_count" in result
        assert result["slide_count"] >= 3
        # At least one slide should have extra fields (bullet_count, kpi_count, etc.)
        has_extra = any(
            any(k in s for k in ("bullet_count", "kpi_count", "column_count", "card_count", "step_count"))
            for s in result["slides"]
        )
        assert has_extra, "Expected at least one slide with count metadata"

    def test_get_plan_slide(self) -> None:
        plan = json.loads(plan_presentation(topic="Slide Inspect", num_slides=5))
        plan_id = plan["plan_id"]
        result = json.loads(get_plan_slide(plan_id, slide_index=0))
        assert result["plan_id"] == plan_id
        assert result["slide_index"] == 0
        assert "slide" in result
        assert result["slide"]["slide_type"] == "title"

    def test_get_plan_slide_invalid_index(self) -> None:
        plan = json.loads(plan_presentation(topic="Bad Index", num_slides=5))
        plan_id = plan["plan_id"]
        with pytest.raises(ToolError, match="out of range"):
            get_plan_slide(plan_id, slide_index=99)


class TestOptimizePlan:
    """Tests for optimize_plan tool (server-side design optimization)."""

    def test_optimize_plan_basic(self) -> None:
        plan = json.loads(plan_presentation(topic="Optimize Test", num_slides=5))
        plan_id = plan["plan_id"]
        result = json.loads(optimize_plan(plan_id))
        assert result["status"] == "success"
        assert result["plan_id"] == plan_id
        assert "changes_made" in result
        assert result["slide_count"] >= 3

    def test_optimize_plan_with_brand(self) -> None:
        plan = json.loads(plan_presentation(topic="Brand Optimize", num_slides=5))
        plan_id = plan["plan_id"]
        result = json.loads(optimize_plan(plan_id, brand_preset="consulting"))
        assert result["status"] == "success"

    def test_optimize_plan_invalid_id(self) -> None:
        with pytest.raises(ToolError, match="not found"):
            optimize_plan("nonexistent123")

    def test_optimize_plan_then_render(self, tmp_path: Path) -> None:
        """Verify optimize_plan updates cached plan and create_from_plan uses it."""
        plan = json.loads(plan_presentation(topic="End-to-End", num_slides=5))
        plan_id = plan["plan_id"]
        # Optimize
        opt = json.loads(optimize_plan(plan_id))
        assert opt["status"] == "success"
        # Render the optimized version
        out = str(tmp_path / "optimized.pptx")
        render = json.loads(create_from_plan(plan_id, out, optimize=False))
        assert render["status"] == "success"
        assert Path(render["output_path"]).exists()


class TestTokenOptimization:
    """Tests for token optimization constraints."""

    def test_generate_presentation_max_slides(self, tmp_path: Path) -> None:
        """generate_presentation is hard-capped at 8 slides."""
        out = str(tmp_path / "small.pptx")
        # 8 slides should work
        result = json.loads(generate_presentation(
            topic="Small Deck", output_path=out, num_slides=8,
        ))
        assert result["status"] == "success"

    def test_generate_presentation_schema_cap(self) -> None:
        """generate_presentation schema advertises max 8 slides to AI clients."""
        import inspect
        sig = inspect.signature(generate_presentation)
        # The Field(le=8) is on the num_slides annotation
        num_slides_param = sig.parameters["num_slides"]
        # Default should be 6
        assert num_slides_param.default == 6

    def test_generate_presentation_default_is_6(self, tmp_path: Path) -> None:
        """Default num_slides should be 6 (not 10)."""
        out = str(tmp_path / "default.pptx")
        result = json.loads(generate_presentation(
            topic="Default Size", output_path=out,
        ))
        assert result["status"] == "success"
        # Should produce roughly 6 slides (planner may adjust slightly)
        assert result["slide_count"] <= 8

    def test_review_summary_only(self, tmp_path: Path) -> None:
        """summary_only returns compact review without full issue details."""
        pptx_path = _generate_sample_pptx(tmp_path)
        result = json.loads(review_presentation(str(pptx_path), summary_only=True))
        assert "overall_score" in result
        assert "total_issues" in result
        assert "slides" in result
        # Should NOT have full slide_reviews structure
        assert "slide_reviews" not in result

    def test_review_full_mode(self, tmp_path: Path) -> None:
        """Default review mode returns full details."""
        pptx_path = _generate_sample_pptx(tmp_path)
        result = json.loads(review_presentation(str(pptx_path)))
        assert "overall_score" in result
        assert "slide_reviews" in result


class TestAddSpeakerNotes:
    """Tests for the add_speaker_notes tool."""

    def test_add_notes_to_slide(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=2)
        result = json.loads(add_speaker_notes(str(pptx_path), slide_index=0, notes="These are my notes"))
        assert result["status"] == "success"
        # Verify notes were actually written
        from pptx import Presentation

        prs = Presentation(str(pptx_path))
        assert prs.slides[0].notes_slide.notes_text_frame.text == "These are my notes"

    def test_add_notes_invalid_index(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=2)
        with pytest.raises(ToolError):
            add_speaker_notes(str(pptx_path), slide_index=99, notes="test")

    def test_add_notes_nonexistent_file(self) -> None:
        with pytest.raises(ToolError):
            add_speaker_notes("/nonexistent/deck.pptx", slide_index=0, notes="test")

    def test_update_existing_notes(self, tmp_path: Path) -> None:
        pptx_path = _generate_sample_pptx(tmp_path, num_slides=2)
        add_speaker_notes(str(pptx_path), slide_index=0, notes="First draft")
        add_speaker_notes(str(pptx_path), slide_index=0, notes="Updated notes")
        from pptx import Presentation

        prs = Presentation(str(pptx_path))
        assert prs.slides[0].notes_slide.notes_text_frame.text == "Updated notes"


class TestSpeakerNotesGeneration:
    """Tests for speaker notes in generated presentations."""

    def test_generated_deck_has_notes(self, tmp_path: Path) -> None:
        """Generated presentations should include speaker notes."""
        out = str(tmp_path / "notes_deck.pptx")
        result = json.loads(generate_presentation(topic="Test Topic", output_path=out, num_slides=5))
        assert result["status"] == "success"
        from pptx import Presentation

        prs = Presentation(out)
        notes_count = sum(
            1 for slide in prs.slides
            if slide.has_notes_slide and slide.notes_slide.notes_text_frame.text.strip()
        )
        assert notes_count >= 3, f"Expected at least 3 slides with notes, got {notes_count}"


class TestRestylePresentation:
    """Tests for the restyle_presentation tool."""

    def test_restyle_basic(self, tmp_path: Path) -> None:
        """Generate a deck, then restyle it with a different brand."""
        source = _generate_sample_pptx(tmp_path, num_slides=3)
        restyled = str(tmp_path / "restyled.pptx")
        result = json.loads(restyle_presentation(str(source), restyled, brand_preset="consulting"))
        assert result["status"] == "success"
        assert result["slide_count"] >= 2
        assert Path(result["output_path"]).exists()

    def test_restyle_preserves_content(self, tmp_path: Path) -> None:
        """Restyled deck should contain original text content."""
        source = _generate_sample_pptx(tmp_path, num_slides=2)
        restyled = str(tmp_path / "restyled.pptx")
        result = json.loads(restyle_presentation(str(source), restyled))
        assert result["status"] == "success"
        # Read back and verify content preserved
        read_result = json.loads(read_presentation(restyled))
        assert len(read_result["slides"]) >= 2

    def test_restyle_nonexistent_file(self) -> None:
        with pytest.raises(ToolError):
            restyle_presentation("/nonexistent/deck.pptx", "/tmp/out.pptx")

    def test_restyle_returns_detected_types(self, tmp_path: Path) -> None:
        """Restyle result should include detected slide types."""
        source = _generate_sample_pptx(tmp_path, num_slides=3)
        restyled = str(tmp_path / "restyled.pptx")
        result = json.loads(restyle_presentation(str(source), restyled))
        assert "detected_types" in result
        assert isinstance(result["detected_types"], list)
        assert len(result["detected_types"]) == result["slide_count"]

    def test_restyle_with_brand_config_path(self, tmp_path: Path) -> None:
        """Restyle with a brand preset produces a valid deck."""
        source = _generate_sample_pptx(tmp_path, num_slides=2)
        restyled = str(tmp_path / "restyled_exec.pptx")
        result = json.loads(restyle_presentation(str(source), restyled, brand_preset="executive"))
        assert result["status"] == "success"
        assert Path(result["output_path"]).exists()
