"""Tests for Phase 1 features: asymmetric columns, gradient backgrounds,
alternating row shading, custom color tokens, and spec parser."""

from __future__ import annotations

import json
from pathlib import Path

import pytest
from pptx import Presentation

from pptx_mcp.brand.defaults import executive_default
from pptx_mcp.engine.generator import DeckGenerator
from pptx_mcp.engine.spec_parser import (
    ContentBlock,
    NormalizedSpec,
    SpecSlide,
    extract_spec,
    parse_spec,
    spec_to_presentation,
    _infer_slide_type,
)
from pptx_mcp.models.brand import BrandConfig, ColorPalette
from pptx_mcp.models.slides import (
    ColumnContent,
    PresentationDefinition,
    PresentationMetadata,
    SlideDefinition,
    SlideType,
    TableCell,
    TableElement,
)


# ---------------------------------------------------------------------------
# Asymmetric column_ratio
# ---------------------------------------------------------------------------


class TestAsymmetricColumns:
    """Tests for column_ratio on two_column slides."""

    def test_column_ratio_renders(self, tmp_path: Path) -> None:
        """Two-column slide with column_ratio produces valid PPTX."""
        definition = PresentationDefinition(
            metadata=PresentationMetadata(title="Ratio Test"),
            slides=[
                SlideDefinition(
                    slide_type=SlideType.TWO_COLUMN,
                    title="Asymmetric",
                    column_ratio=0.65,
                    columns=[
                        ColumnContent(title="Wide", bullets=["Point A", "Point B"]),
                        ColumnContent(title="Narrow", bullets=["Point C"]),
                    ],
                ),
            ],
        )
        out = tmp_path / "ratio.pptx"
        DeckGenerator(brand=executive_default()).generate(definition, out)
        assert out.exists()
        prs = Presentation(str(out))
        assert len(prs.slides) == 1

    def test_column_ratio_default_equal(self, tmp_path: Path) -> None:
        """Without column_ratio, two-column should still render normally."""
        definition = PresentationDefinition(
            metadata=PresentationMetadata(title="Default Ratio"),
            slides=[
                SlideDefinition(
                    slide_type=SlideType.TWO_COLUMN,
                    title="Equal",
                    columns=[
                        ColumnContent(title="Left", bullets=["A"]),
                        ColumnContent(title="Right", bullets=["B"]),
                    ],
                ),
            ],
        )
        out = tmp_path / "default_ratio.pptx"
        DeckGenerator(brand=executive_default()).generate(definition, out)
        assert out.exists()

    def test_column_ratio_validation(self) -> None:
        """column_ratio outside 0.2-0.8 should fail validation."""
        from pydantic import ValidationError

        with pytest.raises(ValidationError):
            SlideDefinition(
                slide_type=SlideType.TWO_COLUMN,
                title="Bad",
                column_ratio=0.1,
            )
        with pytest.raises(ValidationError):
            SlideDefinition(
                slide_type=SlideType.TWO_COLUMN,
                title="Bad",
                column_ratio=0.9,
            )


# ---------------------------------------------------------------------------
# Gradient background
# ---------------------------------------------------------------------------


class TestGradientBackground:
    """Tests for background_gradient on slides."""

    def test_gradient_renders(self, tmp_path: Path) -> None:
        """Slide with gradient background renders without error."""
        definition = PresentationDefinition(
            metadata=PresentationMetadata(title="Gradient Test"),
            slides=[
                SlideDefinition(
                    slide_type=SlideType.CONTENT,
                    title="Gradient Slide",
                    background_gradient=["#1B2A4A", "#E8913A"],
                    bullets=["Test content"],
                ),
            ],
        )
        out = tmp_path / "gradient.pptx"
        DeckGenerator(brand=executive_default()).generate(definition, out)
        assert out.exists()
        prs = Presentation(str(out))
        assert len(prs.slides) == 1

    def test_gradient_three_stops(self, tmp_path: Path) -> None:
        """Three-color gradient should work."""
        definition = PresentationDefinition(
            metadata=PresentationMetadata(title="3-Stop Gradient"),
            slides=[
                SlideDefinition(
                    slide_type=SlideType.SECTION,
                    title="Multi-stop",
                    background_gradient=["#FF0000", "#00FF00", "#0000FF"],
                ),
            ],
        )
        out = tmp_path / "gradient3.pptx"
        DeckGenerator(brand=executive_default()).generate(definition, out)
        assert out.exists()

    def test_gradient_overrides_bg_color(self, tmp_path: Path) -> None:
        """When both background_color and background_gradient set, gradient wins."""
        definition = PresentationDefinition(
            metadata=PresentationMetadata(title="Override Test"),
            slides=[
                SlideDefinition(
                    slide_type=SlideType.CONTENT,
                    title="Override",
                    background_color="#FF0000",
                    background_gradient=["#000000", "#FFFFFF"],
                    bullets=["Test"],
                ),
            ],
        )
        out = tmp_path / "override.pptx"
        DeckGenerator(brand=executive_default()).generate(definition, out)
        assert out.exists()


# ---------------------------------------------------------------------------
# Alternating row shading
# ---------------------------------------------------------------------------


class TestAlternateRowShading:
    """Tests for alternate_row_shading on TableElement."""

    def test_shading_renders(self, tmp_path: Path) -> None:
        """Table with alternate_row_shading produces valid PPTX."""
        definition = PresentationDefinition(
            metadata=PresentationMetadata(title="Table Shading"),
            slides=[
                SlideDefinition(
                    slide_type=SlideType.TABLE,
                    title="Shaded Table",
                    elements=[
                        TableElement(
                            rows=[
                                [TableCell(text="Name"), TableCell(text="Value")],
                                [TableCell(text="A"), TableCell(text="100")],
                                [TableCell(text="B"), TableCell(text="200")],
                                [TableCell(text="C"), TableCell(text="300")],
                                [TableCell(text="D"), TableCell(text="400")],
                            ],
                            alternate_row_shading=True,
                        ),
                    ],
                ),
            ],
        )
        out = tmp_path / "shaded_table.pptx"
        DeckGenerator(brand=executive_default()).generate(definition, out)
        assert out.exists()
        prs = Presentation(str(out))
        assert len(prs.slides) == 1

    def test_shading_default_off(self) -> None:
        """Default alternate_row_shading is False."""
        t = TableElement(rows=[[TableCell(text="A")]])
        assert t.alternate_row_shading is False


# ---------------------------------------------------------------------------
# Custom color tokens
# ---------------------------------------------------------------------------


class TestCustomColorTokens:
    """Tests for custom_tokens on ColorPalette."""

    def test_custom_tokens_in_brand(self) -> None:
        """BrandConfig with custom_tokens should validate."""
        brand = BrandConfig(
            name="Custom Test",
            colors=ColorPalette(
                primary="#000000",
                secondary="#333333",
                accent="#FF0000",
                custom_tokens={"surface": "#F5F5F5", "border": "#E0E0E0"},
            ),
        )
        assert brand.colors.custom_tokens["surface"] == "#F5F5F5"
        assert len(brand.colors.custom_tokens) == 2

    def test_empty_custom_tokens_default(self) -> None:
        """Default custom_tokens is empty dict."""
        palette = ColorPalette(primary="#000", secondary="#333", accent="#F00")
        assert palette.custom_tokens == {}

    def test_custom_tokens_renders(self, tmp_path: Path) -> None:
        """Brand with custom_tokens should not break rendering."""
        brand = BrandConfig(
            name="Token Test",
            colors=ColorPalette(
                primary="#1B2A4A",
                secondary="#2D5F8A",
                accent="#E8913A",
                custom_tokens={"callout_bg": "#FFF3E0", "badge_text": "#333333"},
            ),
        )
        definition = PresentationDefinition(
            metadata=PresentationMetadata(title="Token Test Deck"),
            slides=[
                SlideDefinition(slide_type=SlideType.CONTENT, title="Test", bullets=["A"]),
            ],
        )
        out = tmp_path / "tokens.pptx"
        DeckGenerator(brand=brand).generate(definition, out)
        assert out.exists()


# ---------------------------------------------------------------------------
# Spec parser
# ---------------------------------------------------------------------------


SAMPLE_SPEC = """# Automation Opportunity Assessment: MXP

## Global Design System

### Color Palette
- `primary`: #1A3C6E
- `secondary`: #3B6FA0
- `accent`: #D4A843
- `background`: #FFFFFF
- `text_primary`: #1A1A1A
- `text_secondary`: #4A4A5A

### Typography
- Section Header: 28pt
- Card Title: 16pt
- Body: 14pt

## Slide 1: Title Slide
Automation Opportunity Assessment: MXP

## Slide 2: Executive Summary
- Revenue grew 25% YoY
- Customer satisfaction at 92%
- 3 key initiatives launched

## Slide 3: Key Metrics Dashboard
- $1.2M: Monthly Revenue
- 92%: Customer Retention
- 67: NPS Score

## Slide 4: Implementation Phases
| Phase | Timeline | Status |
|-------|----------|--------|
| Discovery | Q1 2025 | Complete |
| Design | Q2 2025 | In Progress |
| Build | Q3 2025 | Planned |

## Slide 5: Next Steps
- Finalize budget
- Hire team leads
- Launch pilot program
"""


class TestSpecParser:
    """Tests for the markdown spec parser."""

    def test_extract_spec_basic(self) -> None:
        """extract_spec should parse slides and global config."""
        spec = extract_spec(SAMPLE_SPEC)
        assert spec.title == "Automation Opportunity Assessment: MXP"
        assert len(spec.slides) == 5

    def test_extract_color_tokens(self) -> None:
        """Color tokens should be extracted from the design system section."""
        spec = extract_spec(SAMPLE_SPEC)
        color_names = {t.name for t in spec.global_config.colors}
        assert "primary" in color_names
        assert "accent" in color_names

    def test_slide_titles(self) -> None:
        """Each slide should have the correct title."""
        spec = extract_spec(SAMPLE_SPEC)
        assert spec.slides[0].title == "Title Slide"
        assert spec.slides[1].title == "Executive Summary"

    def test_bullet_extraction(self) -> None:
        """Bullets should be extracted from slide content."""
        spec = extract_spec(SAMPLE_SPEC)
        # Slide 2 (Executive Summary) has bullets
        blocks = spec.slides[1].content_blocks
        bullet_blocks = [b for b in blocks if b.block_type == "bullets"]
        assert len(bullet_blocks) >= 1
        items = bullet_blocks[0].content["items"]
        assert len(items) == 3

    def test_table_extraction(self) -> None:
        """Tables should be extracted from markdown pipe syntax."""
        spec = extract_spec(SAMPLE_SPEC)
        # Slide 4 (Implementation Phases) has a table
        blocks = spec.slides[3].content_blocks
        table_blocks = [b for b in blocks if b.block_type == "table"]
        assert len(table_blocks) >= 1
        rows = table_blocks[0].content["rows"]
        assert len(rows) >= 3

    def test_spec_to_presentation(self) -> None:
        """spec_to_presentation should produce valid PresentationDefinition."""
        spec = extract_spec(SAMPLE_SPEC)
        definition, brand = spec_to_presentation(spec)
        assert len(definition.slides) == 5
        assert definition.metadata.title == "Automation Opportunity Assessment: MXP"

    def test_brand_extraction(self) -> None:
        """Brand config should be extracted from color tokens."""
        spec = extract_spec(SAMPLE_SPEC)
        _, brand = spec_to_presentation(spec)
        assert brand is not None
        assert brand.colors.primary == "#1A3C6E"
        assert brand.colors.accent == "#D4A843"

    def test_parse_spec_end_to_end(self) -> None:
        """parse_spec should work end-to-end."""
        definition, brand = parse_spec(SAMPLE_SPEC)
        assert len(definition.slides) == 5
        assert brand is not None

    def test_parse_spec_generates_pptx(self, tmp_path: Path) -> None:
        """Parsed spec should produce a valid PPTX when generated."""
        definition, brand = parse_spec(SAMPLE_SPEC)
        out = tmp_path / "spec_output.pptx"
        generator = DeckGenerator(brand=brand or executive_default())
        generator.generate(definition, out)
        assert out.exists()
        prs = Presentation(str(out))
        assert len(prs.slides) == 5

    def test_infer_slide_type_title(self) -> None:
        """Title-related keywords should map to TITLE."""
        assert _infer_slide_type("Title Slide", []) == SlideType.TITLE

    def test_infer_slide_type_dashboard(self) -> None:
        """Dashboard keywords should map to DASHBOARD."""
        assert _infer_slide_type("Key Metrics Dashboard", []) == SlideType.DASHBOARD

    def test_infer_slide_type_table_from_content(self) -> None:
        """Table block should infer TABLE type."""
        blocks = [ContentBlock(block_type="table", content={"rows": [["a", "b"]]})]
        assert _infer_slide_type("Implementation Phases", blocks) == SlideType.TABLE

    def test_empty_spec(self) -> None:
        """Empty markdown should produce empty spec."""
        spec = extract_spec("")
        assert len(spec.slides) == 0

    def test_spec_without_global_config(self) -> None:
        """Spec without color tokens should still parse."""
        md = """# Simple Deck

## Slide 1: Hello
- Point 1
- Point 2
"""
        definition, brand = parse_spec(md)
        assert len(definition.slides) == 1
        assert brand is None


# ---------------------------------------------------------------------------
# parse_spec MCP tool (server integration)
# ---------------------------------------------------------------------------


class TestParseSpecTool:
    """Tests for the parse_spec MCP tool."""

    def _get_server(self):
        import sys
        from unittest.mock import patch
        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):
            cached = sys.modules.pop("pptx_mcp.server", None)
            try:
                import pptx_mcp.server as server_mod
                return server_mod
            finally:
                pass

    def test_parse_spec_tool_returns_json(self) -> None:
        server = self._get_server()
        result = json.loads(server.parse_spec(markdown=SAMPLE_SPEC))
        assert result["slide_count"] == 5
        assert result["spec_brand_detected"] is True
        assert "definition" in result

    def test_parse_spec_tool_generates_pptx(self, tmp_path: Path) -> None:
        server = self._get_server()
        out = str(tmp_path / "spec_tool.pptx")
        result = json.loads(server.parse_spec(markdown=SAMPLE_SPEC, output_path=out))
        assert result["status"] == "success"
        assert Path(result["output_path"]).exists()

    def test_parse_spec_tool_with_preset(self, tmp_path: Path) -> None:
        server = self._get_server()
        out = str(tmp_path / "spec_preset.pptx")
        result = json.loads(server.parse_spec(
            markdown=SAMPLE_SPEC,
            output_path=out,
            brand_preset="consulting",
        ))
        assert result["status"] == "success"
