"""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()

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_slide_templates = server.get_slide_templates
move_slide = server.move_slide
plan_presentation = server.plan_presentation
read_presentation = server.read_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 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 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"


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",
            "team", "closing", "swot", "funnel", "agenda",
        }
        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"}

    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.

    Note: The underlying _delete_slide helper has a known namespace bug
    (uses 'r:id' instead of the full qualified namespace) that causes
    valid delete operations to fail with a KeyError. The tests below
    verify the tool's error-handling behavior and the out-of-range check.
    """

    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"}
        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
