"""Tests for brand extraction engine."""

from __future__ import annotations

import json
from pathlib import Path

import pytest
from pptx import Presentation

from pptx_mcp.brand.defaults import PRESETS, get_preset
from pptx_mcp.engine.brand_extractor import (
    _build_color_palette,
    _build_font_config,
    _is_chromatic,
    _is_near_black,
    _is_near_white,
    extract_brand,
)
from pptx_mcp.engine.generator import DeckGenerator
from pptx_mcp.models.brand import BrandConfig, ColorPalette
from pptx_mcp.models.slides import (
    PresentationDefinition,
    PresentationMetadata,
    SlideDefinition,
    SlideType,
)
from pptx_mcp.utils.errors import ParseError


def _generate_test_deck(tmp_path: Path, preset_name: str, filename: str = "test.pptx") -> Path:
    """Generate a simple deck with a known preset for extraction testing."""
    brand = get_preset(preset_name)
    gen = DeckGenerator(brand=brand)
    defn = PresentationDefinition(
        metadata=PresentationMetadata(title=f"Test {preset_name.title()} Deck"),
        slides=[
            SlideDefinition(
                slide_type=SlideType.TITLE,
                title=f"Test {preset_name.title()} Presentation",
                subtitle="A test deck for brand extraction",
            ),
            SlideDefinition(
                slide_type=SlideType.CONTENT,
                title="Key Findings",
                bullets=[
                    "Revenue grew 15% year-over-year",
                    "Customer retention at 92%",
                    "NPS improved to 67 this quarter",
                ],
            ),
            SlideDefinition(
                slide_type=SlideType.DASHBOARD,
                title="Key Metrics",
                kpis=[
                    {"value": "$1.2M", "label": "Revenue", "change": "+15%", "trend": "up"},
                    {"value": "92%", "label": "Retention", "change": "+3%", "trend": "up"},
                ],
            ),
        ],
    )
    out = tmp_path / filename
    gen.generate(defn, str(out))
    return out


class TestExtractBrandFromPresets:
    """Test extracting brand from decks generated with known presets."""

    def test_extract_executive(self, tmp_path: Path):
        """Extract brand from executive preset deck, verify primary color detected."""
        deck_path = _generate_test_deck(tmp_path, "executive")
        result = extract_brand(deck_path)
        assert isinstance(result, BrandConfig)
        assert result.name  # Has a name

    def test_extract_consulting(self, tmp_path: Path):
        """Extract brand from consulting preset deck."""
        deck_path = _generate_test_deck(tmp_path, "consulting")
        result = extract_brand(deck_path)
        assert isinstance(result, BrandConfig)
        # Should detect Aptos as heading font (consulting uses Aptos)
        assert result.fonts.heading_font == "Aptos"

    def test_extract_startup(self, tmp_path: Path):
        """Extract brand from startup preset deck."""
        deck_path = _generate_test_deck(tmp_path, "startup")
        result = extract_brand(deck_path)
        assert isinstance(result, BrandConfig)
        # Startup uses Aptos Display for headings
        assert "Aptos" in result.fonts.heading_font

    def test_extract_colors_are_hex(self, tmp_path: Path):
        """Extracted colors should be valid hex strings."""
        deck_path = _generate_test_deck(tmp_path, "executive")
        result = extract_brand(deck_path)
        assert result.colors.primary.startswith("#")
        assert result.colors.secondary.startswith("#")
        assert result.colors.accent.startswith("#")
        assert len(result.colors.primary) == 7  # #RRGGBB

    def test_extract_font_sizes_reasonable(self, tmp_path: Path):
        """Extracted font sizes should be within reasonable ranges."""
        deck_path = _generate_test_deck(tmp_path, "executive")
        result = extract_brand(deck_path)
        assert 20 <= result.fonts.title_size <= 50
        assert 14 <= result.fonts.heading_size <= 40
        assert 8 <= result.fonts.body_size <= 24


class TestExtractBrandEdgeCases:
    """Test edge cases for brand extraction."""

    def test_empty_presentation(self, tmp_path: Path):
        """Extract brand from an empty presentation (no slides)."""
        out = tmp_path / "empty.pptx"
        prs = Presentation()
        prs.save(str(out))
        result = extract_brand(out)
        assert isinstance(result, BrandConfig)
        assert result.name == "empty"

    def test_single_blank_slide(self, tmp_path: Path):
        """Extract brand from a single blank slide."""
        out = tmp_path / "blank.pptx"
        prs = Presentation()
        prs.slides.add_slide(prs.slide_layouts[6])  # blank layout
        prs.save(str(out))
        result = extract_brand(out)
        assert isinstance(result, BrandConfig)

    def test_file_not_found(self):
        """Should raise ParseError for non-existent file."""
        with pytest.raises(ParseError, match="File not found"):
            extract_brand("/nonexistent/path/to/file.pptx")

    def test_invalid_file(self, tmp_path: Path):
        """Should raise ParseError for invalid PPTX."""
        bad_file = tmp_path / "bad.pptx"
        bad_file.write_text("not a pptx file")
        with pytest.raises(ParseError, match="Failed to open"):
            extract_brand(bad_file)


class TestColorHelpers:
    """Test color classification helper functions."""

    def test_is_near_white(self):
        assert _is_near_white("#FFFFFF") is True
        assert _is_near_white("#F5F5F5") is True
        assert _is_near_white("#F0F0F0") is True
        assert _is_near_white("#CCCCCC") is False
        assert _is_near_white("#1A2B3C") is False

    def test_is_near_black(self):
        assert _is_near_black("#000000") is True
        assert _is_near_black("#1A1A1A") is True
        assert _is_near_black("#303030") is True
        assert _is_near_black("#555555") is False
        assert _is_near_black("#FFFFFF") is False

    def test_is_chromatic(self):
        assert _is_chromatic("#FF0000") is True
        assert _is_chromatic("#1B2A4A") is True
        assert _is_chromatic("#E8913A") is True
        assert _is_chromatic("#FFFFFF") is False
        assert _is_chromatic("#000000") is False
        assert _is_chromatic("#808080") is False

    def test_is_near_white_short_hex(self):
        """Short hex strings should return False safely."""
        assert _is_near_white("#FFF") is False
        assert _is_near_black("#000") is False
        assert _is_chromatic("#F00") is False


class TestBuildColorPalette:
    """Test the color palette building logic."""

    def test_no_colors_uses_defaults(self):
        from collections import Counter
        palette = _build_color_palette(Counter(), Counter(), Counter(), Counter())
        assert palette.primary == "#1B2A4A"
        assert palette.background == "#FFFFFF"

    def test_chromatic_fills_become_primary(self):
        from collections import Counter
        fills = Counter({"#2D5F8A": 10, "#E8913A": 5, "#1B2A4A": 15})
        palette = _build_color_palette(fills, Counter(), Counter(), Counter())
        assert palette.primary == "#1B2A4A"
        assert palette.secondary == "#2D5F8A"
        assert palette.accent == "#E8913A"


class TestBuildFontConfig:
    """Test the font config building logic."""

    def test_no_fonts_uses_defaults(self):
        from collections import Counter
        config = _build_font_config(Counter(), Counter(), Counter(), Counter())
        assert config.heading_font == "Calibri"
        assert config.body_font == "Calibri"

    def test_most_common_font_wins(self):
        from collections import Counter
        headings = Counter({"Aptos": 20, "Arial": 5})
        bodies = Counter({"Aptos": 30, "Verdana": 2})
        config = _build_font_config(headings, bodies, Counter({36: 10, 28: 8}), Counter({16: 25}))
        assert config.heading_font == "Aptos"
        assert config.body_font == "Aptos"
        assert config.title_size == 36
        assert config.body_size == 16


class TestMcpTool:
    """Test the MCP tool wrapper for extract_brand."""

    def test_tool_returns_json(self, tmp_path: Path):
        """The server tool should return valid JSON."""
        # We test via the engine function since the MCP tool wraps it
        deck_path = _generate_test_deck(tmp_path, "executive")
        result = extract_brand(deck_path)
        # Should be serializable
        data = json.loads(result.model_dump_json())
        assert "name" in data
        assert "colors" in data
        assert "fonts" in data

    def test_save_to_json(self, tmp_path: Path):
        """Extract and save to JSON file."""
        deck_path = _generate_test_deck(tmp_path, "consulting")
        result = extract_brand(deck_path)
        save_path = tmp_path / "extracted_brand.json"
        save_path.write_text(result.model_dump_json(indent=2))
        assert save_path.exists()
        loaded = json.loads(save_path.read_text())
        assert loaded["colors"]["primary"].startswith("#")

    def test_roundtrip_brand(self, tmp_path: Path):
        """Extract brand, then use it to generate a new deck (smoke test)."""
        # Generate with known brand
        deck_path = _generate_test_deck(tmp_path, "executive")
        # Extract brand
        extracted = extract_brand(deck_path)
        # Use extracted brand to generate a new deck
        gen = DeckGenerator(brand=extracted)
        defn = PresentationDefinition(
            metadata=PresentationMetadata(title="Roundtrip Test"),
            slides=[
                SlideDefinition(
                    slide_type=SlideType.TITLE,
                    title="Roundtrip",
                    subtitle="Test",
                ),
            ],
        )
        out = tmp_path / "roundtrip.pptx"
        result_path = gen.generate(defn, str(out))
        assert Path(result_path).exists()
