"""Tests for Phase 2 analysis bug fixes (P0-1, P1-2, P2-4, P3-1/2/3)."""

from __future__ import annotations

from pathlib import Path

import pytest
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE

from pptx_mcp.brand.defaults import PRESETS
from pptx_mcp.engine.generator import DeckGenerator
from pptx_mcp.engine.layouts import _add_slide_title
from pptx_mcp.engine.parser import parse_presentation, _extract_fill_color
from pptx_mcp.engine.shapes import parse_hex_color
from pptx_mcp.models.brand import BrandConfig, ShapeStyleConfig
from pptx_mcp.models.slides import (
    BulletList,
    ColumnContent,
    PresentationDefinition,
    PresentationMetadata,
    ShapeElement,
    ShapeType,
    SlideDefinition,
    SlideType,
    TableCell,
    TableElement,
    TextBox,
    TextStyle,
    Paragraph,
    TextRun,
    Position,
)


_TITLE_SLIDE = SlideDefinition(slide_type=SlideType.TITLE, title="Deck Title")


def _make_defn(*slides: SlideDefinition) -> PresentationDefinition:
    return PresentationDefinition(
        metadata=PresentationMetadata(title="Test"),
        slides=list(slides),
    )


def _make_defn_with_title(*slides: SlideDefinition) -> PresentationDefinition:
    """Wrap slides with a title slide so test slides aren't at index 0."""
    return PresentationDefinition(
        metadata=PresentationMetadata(title="Test"),
        slides=[_TITLE_SLIDE, *slides],
    )


def _generate(defn: PresentationDefinition, tmp_path: Path, brand: BrandConfig | None = None) -> Path:
    out = tmp_path / "test.pptx"
    gen = DeckGenerator(brand=brand)
    gen.generate(defn, out)
    return out


# ---------------------------------------------------------------------------
# Fix 1: P0-1 — Content slide multi-column + subtitle
# ---------------------------------------------------------------------------


class TestContentMultiColumn:
    """Content slide should render columns when definition.columns is populated."""

    def test_three_column_content(self, tmp_path: Path) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Three Columns",
            columns=[
                ColumnContent(title="Col A", bullets=["A1", "A2"]),
                ColumnContent(title="Col B", bullets=["B1"]),
                ColumnContent(title="Col C", bullets=["C1", "C2", "C3"]),
            ],
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        texts = [s.text_frame.text for s in shapes if hasattr(s, "text_frame") and s.text_frame.text.strip()]
        # All column titles and bullet items should appear
        assert "Col A" in texts
        assert "Col B" in texts
        assert "Col C" in texts
        assert any("A1" in t for t in texts)
        assert any("C3" in t for t in texts)

    def test_four_column_content(self, tmp_path: Path) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Four Columns",
            columns=[
                ColumnContent(title=f"Col {i}", bullets=[f"Item {i}"])
                for i in range(4)
            ],
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        texts = [s.text_frame.text for s in shapes if hasattr(s, "text_frame") and s.text_frame.text.strip()]
        assert "Col 3" in texts

    def test_content_with_subtitle(self, tmp_path: Path) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Title Here",
            subtitle="Subtitle text for this slide",
            bullets=["Bullet 1"],
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        texts = [s.text_frame.text for s in shapes if hasattr(s, "text_frame") and s.text_frame.text.strip()]
        assert "Subtitle text for this slide" in texts

    def test_content_columns_with_subtitle(self, tmp_path: Path) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Title",
            subtitle="A subtitle",
            columns=[
                ColumnContent(title="Left", bullets=["L1"]),
                ColumnContent(title="Right", bullets=["R1"]),
            ],
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        texts = [s.text_frame.text for s in shapes if hasattr(s, "text_frame") and s.text_frame.text.strip()]
        assert "A subtitle" in texts
        assert "Left" in texts
        assert "Right" in texts

    def test_content_without_columns_unchanged(self, tmp_path: Path) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Simple Content",
            body="Some body text",
            bullets=["Point 1", "Point 2"],
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        texts = [s.text_frame.text for s in shapes if hasattr(s, "text_frame") and s.text_frame.text.strip()]
        assert "Some body text" in texts
        assert any("Point 1" in t for t in texts)


# ---------------------------------------------------------------------------
# Fix 2: P3-1/2/3 — Parser fill colors, shape types, table cell styling
# ---------------------------------------------------------------------------


class TestParserFillAndShapeDetection:
    """Parser should extract fill colors, detect shape types, and capture table cell styles."""

    def test_textbox_fill_color_extracted(self, tmp_path: Path) -> None:
        """Generate PPTX with a filled textbox, then parse — fill_color should be extracted."""
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Content Slide",
            body="Main body",
            elements=[
                TextBox(
                    paragraphs=[Paragraph(runs=[TextRun(text="Filled box")])],
                    position=Position(left=1.0, top=5.0, width=4.0, height=1.0),
                    fill_color="#FF5500",
                ),
            ],
        )
        path = _generate(_make_defn_with_title(slide), tmp_path)
        defn = parse_presentation(path)
        # Slide at index 1 (after title)
        textboxes = [e for e in defn.slides[1].elements if isinstance(e, TextBox)]
        filled = [t for t in textboxes if t.fill_color is not None]
        assert len(filled) >= 1
        assert filled[0].fill_color == "#FF5500"

    def test_shape_type_rounded_rectangle(self, tmp_path: Path) -> None:
        """Rounded rectangle auto shapes should be detected correctly."""
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Shape Rendering Test Slide",
            body="This slide contains a rounded rectangle shape for testing parser detection",
            bullets=["First point about shapes", "Second point about rendering"],
            elements=[
                ShapeElement(
                    shape_type=ShapeType.ROUNDED_RECTANGLE,
                    position=Position(left=1.0, top=5.0, width=3.0, height=2.0),
                    fill_color="#00AA00",
                ),
            ],
        )
        path = _generate(_make_defn_with_title(slide), tmp_path)
        defn = parse_presentation(path)
        shapes = [e for e in defn.slides[1].elements if isinstance(e, ShapeElement)]
        rounded = [s for s in shapes if s.shape_type == ShapeType.ROUNDED_RECTANGLE]
        assert len(rounded) >= 1

    def test_shape_type_chevron_detected(self, tmp_path: Path) -> None:
        """Chevron shapes should be detected via auto_shape_type."""
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Chevron Shape Detection Test",
            body="This slide has a chevron shape that should be correctly identified by the parser",
            bullets=["Parser should detect shape types", "Not hardcode RECTANGLE"],
            elements=[
                ShapeElement(
                    shape_type=ShapeType.CHEVRON,
                    position=Position(left=1.0, top=5.0, width=3.0, height=1.5),
                    fill_color="#0055FF",
                ),
            ],
        )
        path = _generate(_make_defn_with_title(slide), tmp_path)
        defn = parse_presentation(path)
        shapes = [e for e in defn.slides[1].elements if isinstance(e, ShapeElement)]
        chevrons = [s for s in shapes if s.shape_type == ShapeType.CHEVRON]
        assert len(chevrons) >= 1

    def test_shape_fill_color_extracted(self, tmp_path: Path) -> None:
        """Shape fill_color should be extracted from parsed shapes."""
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Oval Shape Fill Test",
            body="This slide tests that the parser correctly extracts shape fill colors",
            bullets=["Fill color extraction", "Round-trip fidelity"],
            elements=[
                ShapeElement(
                    shape_type=ShapeType.OVAL,
                    position=Position(left=2.0, top=5.0, width=2.0, height=2.0),
                    fill_color="#FF0000",
                ),
            ],
        )
        path = _generate(_make_defn_with_title(slide), tmp_path)
        defn = parse_presentation(path)
        shapes = [e for e in defn.slides[1].elements if isinstance(e, ShapeElement)]
        ovals = [s for s in shapes if s.shape_type == ShapeType.OVAL]
        assert len(ovals) >= 1
        assert ovals[0].fill_color == "#FF0000"

    def test_shape_no_fill_returns_none(self, tmp_path: Path) -> None:
        """Shape with no fill should have fill_color=None."""
        class FakeShape:
            class fill:
                type = None
        assert _extract_fill_color(FakeShape()) is None

    def test_shape_border_extracted(self, tmp_path: Path) -> None:
        """Shape border_color and border_width should be extracted."""
        slide = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Diamond Border Test Slide",
            body="This slide tests that the parser correctly extracts border styling from shapes",
            bullets=["Border color extraction", "Border width extraction"],
            elements=[
                ShapeElement(
                    shape_type=ShapeType.DIAMOND,
                    position=Position(left=2.0, top=5.0, width=2.0, height=2.0),
                    fill_color="#00FF00",
                    border_color="#000000",
                    border_width=2.0,
                ),
            ],
        )
        path = _generate(_make_defn_with_title(slide), tmp_path)
        defn = parse_presentation(path)
        shapes = [e for e in defn.slides[1].elements if isinstance(e, ShapeElement)]
        bordered = [s for s in shapes if s.border_color is not None]
        assert len(bordered) >= 1
        assert bordered[0].border_width is not None
        assert bordered[0].border_width > 0

    def test_table_cell_fill_color(self, tmp_path: Path) -> None:
        """Table cells with fill_color should have it extracted on parse."""
        slide = SlideDefinition(
            slide_type=SlideType.TABLE,
            title="Table Test",
            elements=[
                TableElement(
                    rows=[
                        [TableCell(text="Header", fill_color="#1A1A2E")],
                        [TableCell(text="Data")],
                    ],
                    position=Position(left=1.0, top=2.0, width=8.0, height=2.0),
                    header_row=False,
                ),
            ],
        )
        path = _generate(_make_defn_with_title(slide), tmp_path)
        defn = parse_presentation(path)
        tables = [e for e in defn.slides[1].elements if isinstance(e, TableElement)]
        assert len(tables) >= 1
        first_cell = tables[0].rows[0][0]
        assert first_cell.fill_color is not None

    def test_table_cell_text_style(self, tmp_path: Path) -> None:
        """Table cells should have text style extracted (bold, color)."""
        slide = SlideDefinition(
            slide_type=SlideType.TABLE,
            title="Styled Table",
            elements=[
                TableElement(
                    rows=[
                        [TableCell(text="Bold Header", fill_color="#003366",
                                   style=TextStyle(bold=True, color="#FFFFFF"))],
                        [TableCell(text="Normal")],
                    ],
                    position=Position(left=1.0, top=2.0, width=8.0, height=2.0),
                    header_row=False,
                ),
            ],
        )
        path = _generate(_make_defn_with_title(slide), tmp_path)
        defn = parse_presentation(path)
        tables = [e for e in defn.slides[1].elements if isinstance(e, TableElement)]
        assert len(tables) >= 1
        first_cell = tables[0].rows[0][0]
        assert first_cell.style is not None


# ---------------------------------------------------------------------------
# Fix 3: P1-2 — Configurable accent bar
# ---------------------------------------------------------------------------


class TestAccentBar:
    """Accent underline bar should be configurable via show_accent_bar."""

    def _count_accent_bars(self, slide) -> int:
        """Count thin rectangle shapes that look like accent bars."""
        count = 0
        for shape in slide.shapes:
            if hasattr(shape, "shape_type"):
                # Accent bars are thin rectangles (height ~0.04")
                h_inches = shape.height / 914400
                w_inches = shape.width / 914400
                if h_inches < 0.1 and w_inches > 0.5:
                    count += 1
        return count

    def test_accent_bar_present_by_default(self, tmp_path: Path) -> None:
        slide_def = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="With Bar",
            bullets=["Item"],
        )
        path = _generate(_make_defn(slide_def), tmp_path)
        prs = Presentation(str(path))
        bars = self._count_accent_bars(prs.slides[0])
        assert bars >= 1

    def test_accent_bar_hidden(self, tmp_path: Path) -> None:
        brand = list(PRESETS.values())[0].model_copy(
            update={"shape_style": ShapeStyleConfig(show_accent_bar=False)}
        )
        slide_def = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="No Bar",
            bullets=["Item"],
        )
        path = _generate(_make_defn(slide_def), tmp_path, brand=brand)
        prs = Presentation(str(path))
        bars = self._count_accent_bars(prs.slides[0])
        assert bars == 0

    def test_all_presets_default_accent_bar_true(self) -> None:
        for name, brand in PRESETS.items():
            assert brand.shape_style.show_accent_bar is True, (
                f"Preset '{name}' should have show_accent_bar=True by default"
            )


# ---------------------------------------------------------------------------
# Fix 4: P2-4 — Bullet list per-element style priority
# ---------------------------------------------------------------------------


class TestBulletListStylePriority:
    """BulletList with TextStyle should override brand defaults."""

    def test_bullet_list_style_overrides_brand(self, tmp_path: Path) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.BLANK,
            elements=[
                BulletList(
                    items=["Custom styled"],
                    position=Position(left=1.0, top=2.0, width=8.0, height=3.0),
                    style=TextStyle(font_name="Arial Black", font_size=24, color="#FF0000"),
                ),
            ],
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        # Find the bullet list textbox
        for s in shapes:
            if hasattr(s, "text_frame") and "Custom styled" in s.text_frame.text:
                run = s.text_frame.paragraphs[0].runs[0]
                assert run.font.name == "Arial Black"
                assert run.font.size.pt == 24
                assert run.font.color.rgb == parse_hex_color("#FF0000")
                return
        pytest.fail("Bullet list shape not found")

    def test_bullet_list_no_style_uses_brand(self, tmp_path: Path) -> None:
        brand = list(PRESETS.values())[0]
        slide = SlideDefinition(
            slide_type=SlideType.BLANK,
            elements=[
                BulletList(
                    items=["Default styled"],
                    position=Position(left=1.0, top=2.0, width=8.0, height=3.0),
                ),
            ],
        )
        path = _generate(_make_defn(slide), tmp_path, brand=brand)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        for s in shapes:
            if hasattr(s, "text_frame") and "Default styled" in s.text_frame.text:
                run = s.text_frame.paragraphs[0].runs[0]
                assert run.font.name == brand.fonts.body_font
                assert run.font.size.pt == brand.fonts.body_size
                return
        pytest.fail("Bullet list shape not found")

    def test_bullet_list_color_override(self, tmp_path: Path) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.BLANK,
            elements=[
                BulletList(
                    items=["Red bullet"],
                    position=Position(left=1.0, top=2.0, width=8.0, height=3.0),
                    style=TextStyle(color="#CC0000"),
                ),
            ],
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        for s in shapes:
            if hasattr(s, "text_frame") and "Red bullet" in s.text_frame.text:
                run = s.text_frame.paragraphs[0].runs[0]
                assert run.font.color.rgb == parse_hex_color("#CC0000")
                return
        pytest.fail("Bullet list shape not found")


# ---------------------------------------------------------------------------
# Fix 5: P1-3 — Shape border with no fill renders visible stroke
# ---------------------------------------------------------------------------


class TestShapeBorderNoFill:
    """Shape with border_color but no fill_color should have visible borders."""

    def test_border_only_shape_has_visible_line(self, tmp_path: Path) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.BLANK,
            elements=[
                ShapeElement(
                    shape_type=ShapeType.CHEVRON,
                    position=Position(left=2.0, top=2.0, width=3.0, height=1.5),
                    border_color="#69B3E7",
                    border_width=1.5,
                ),
            ],
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        for s in shapes:
            try:
                if s.auto_shape_type is not None:
                    assert s.line.color.rgb is not None
                    assert s.line.width is not None
                    return
            except (ValueError, AttributeError):
                continue
        pytest.fail("Chevron shape not found")

    def test_border_color_without_explicit_width_gets_default(self, tmp_path: Path) -> None:
        """When border_color is set but border_width is not, should default to 1pt."""
        slide = SlideDefinition(
            slide_type=SlideType.BLANK,
            elements=[
                ShapeElement(
                    shape_type=ShapeType.RECTANGLE,
                    position=Position(left=1.0, top=1.0, width=2.0, height=2.0),
                    border_color="#FF0000",
                ),
            ],
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        for s in shapes:
            try:
                if s.auto_shape_type is not None:
                    assert s.line.width is not None
                    assert s.line.width > 0
                    return
            except (ValueError, AttributeError):
                continue
        pytest.fail("Rectangle shape not found")

    def test_no_fill_no_border_hides_line(self, tmp_path: Path) -> None:
        """Shape with no fill and no border should have hidden line."""
        slide = SlideDefinition(
            slide_type=SlideType.BLANK,
            elements=[
                ShapeElement(
                    shape_type=ShapeType.OVAL,
                    position=Position(left=1.0, top=1.0, width=2.0, height=2.0),
                ),
            ],
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        assert len(prs.slides) == 1


# ---------------------------------------------------------------------------
# Fix 6: P2-1 — Title slide style overrides
# ---------------------------------------------------------------------------


class TestTitleSlideStyleOverride:
    """Title slide should respect title_style and subtitle_style overrides."""

    def test_title_style_overrides_brand(self, tmp_path: Path) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.TITLE,
            title="Custom Title",
            title_style=TextStyle(font_name="Georgia", font_size=44, color="#002677"),
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        for s in shapes:
            if hasattr(s, "text_frame") and "Custom Title" in s.text_frame.text:
                run = s.text_frame.paragraphs[0].runs[0]
                assert run.font.name == "Georgia"
                assert run.font.size.pt == 44
                assert run.font.color.rgb == parse_hex_color("#002677")
                return
        pytest.fail("Title shape not found")

    def test_subtitle_style_overrides_brand(self, tmp_path: Path) -> None:
        slide = SlideDefinition(
            slide_type=SlideType.TITLE,
            title="Title",
            subtitle="Styled Sub",
            subtitle_style=TextStyle(font_name="Arial", font_size=18, color="#336699"),
        )
        path = _generate(_make_defn(slide), tmp_path)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        for s in shapes:
            if hasattr(s, "text_frame") and "Styled Sub" in s.text_frame.text:
                run = s.text_frame.paragraphs[0].runs[0]
                assert run.font.name == "Arial"
                assert run.font.size.pt == 18
                assert run.font.color.rgb == parse_hex_color("#336699")
                return
        pytest.fail("Subtitle shape not found")

    def test_no_style_uses_brand_defaults(self, tmp_path: Path) -> None:
        brand = list(PRESETS.values())[0]
        slide = SlideDefinition(
            slide_type=SlideType.TITLE,
            title="Default Title",
        )
        path = _generate(_make_defn(slide), tmp_path, brand=brand)
        prs = Presentation(str(path))
        shapes = list(prs.slides[0].shapes)
        for s in shapes:
            if hasattr(s, "text_frame") and "Default Title" in s.text_frame.text:
                run = s.text_frame.paragraphs[0].runs[0]
                assert run.font.name == brand.fonts.heading_font
                assert run.font.size.pt == brand.fonts.title_size
                return
        pytest.fail("Title shape not found")


# ---------------------------------------------------------------------------
# Fix 7: P0-2 — Title slide parser classification
# ---------------------------------------------------------------------------


class TestTitleSlideClassification:
    """Parser should classify centered title+subtitle slides as TITLE, not TWO_COLUMN."""

    def test_title_slide_round_trip(self, tmp_path: Path) -> None:
        """Generate a title slide, parse it back — should be classified as TITLE."""
        slide = SlideDefinition(
            slide_type=SlideType.TITLE,
            title="Automation Assessment",
            subtitle="Q1 2026 Review",
        )
        path = _generate(_make_defn(slide), tmp_path)
        defn = parse_presentation(path)
        assert defn.slides[0].slide_type == SlideType.TITLE
        assert defn.slides[0].title is not None

    def test_title_slide_not_first_position(self, tmp_path: Path) -> None:
        """Title slide not at index 0 should still be classified as TITLE."""
        content = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Intro",
            body="Some introductory content for context",
            bullets=["Point one", "Point two"],
        )
        title = SlideDefinition(
            slide_type=SlideType.TITLE,
            title="Main Presentation Title",
            subtitle="Department Overview",
        )
        path = _generate(_make_defn(content, title), tmp_path)
        defn = parse_presentation(path)
        assert defn.slides[1].slide_type == SlideType.TITLE
