"""Tests for Stream 3 diagram renderers: waterfall, hub-spoke, cycle, gantt."""

from __future__ import annotations

import pytest
from pptx import Presentation

from pptx_mcp.brand.defaults import PRESETS
from pptx_mcp.engine.diagrams.cycle import render_cycle_slide
from pptx_mcp.engine.diagrams.gantt import render_gantt_slide
from pptx_mcp.engine.diagrams.hub_spoke import render_hub_spoke_slide
from pptx_mcp.engine.diagrams.waterfall import render_waterfall_slide
from pptx_mcp.models.diagrams import CycleStep, GanttTask, SpokeItem, WaterfallBar
from pptx_mcp.models.slides import SlideDefinition, SlideType

SLIDE_WIDTH = 13.333


def _make_slide():
    """Create a blank presentation and return (prs, slide)."""
    prs = Presentation()
    slide = prs.slides.add_slide(prs.slide_layouts[6])  # blank layout
    return prs, slide


# ── Waterfall ──────────────────────────────────────────────────────────


class TestWaterfall:
    def test_basic_waterfall(self, tmp_path):
        brand = PRESETS["executive"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.WATERFALL,
            title="Revenue Bridge",
            waterfall_bars=[
                WaterfallBar(label="Q1", value=100, bar_type="total"),
                WaterfallBar(label="Growth", value=30, bar_type="increase"),
                WaterfallBar(label="Churn", value=-20, bar_type="decrease"),
                WaterfallBar(label="Q2", value=110, bar_type="total"),
            ],
        )
        render_waterfall_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "waterfall.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_waterfall_empty_bars(self, tmp_path):
        brand = PRESETS["technical"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.WATERFALL,
            title="Empty Waterfall",
            waterfall_bars=[],
        )
        render_waterfall_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "waterfall_empty.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_waterfall_single_total(self, tmp_path):
        brand = PRESETS["minimal"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.WATERFALL,
            title="Single",
            waterfall_bars=[WaterfallBar(label="Total", value=50, bar_type="total")],
        )
        render_waterfall_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "waterfall_single.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_waterfall_many_bars(self, tmp_path):
        brand = PRESETS["creative"].model_copy(deep=True)
        prs, slide = _make_slide()
        bars = [WaterfallBar(label="Start", value=200, bar_type="total")]
        for i in range(8):
            bars.append(WaterfallBar(label=f"Change {i+1}", value=(-1) ** i * 10, bar_type="increase" if i % 2 == 0 else "decrease"))
        bars.append(WaterfallBar(label="End", value=200, bar_type="total"))
        defn = SlideDefinition(
            slide_type=SlideType.WATERFALL,
            title="Many Bars",
            waterfall_bars=bars,
        )
        render_waterfall_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "waterfall_many.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_waterfall_custom_colors(self, tmp_path):
        brand = PRESETS["executive"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.WATERFALL,
            title="Custom Colors",
            waterfall_bars=[
                WaterfallBar(label="Base", value=100, bar_type="total", color="#336699"),
                WaterfallBar(label="Add", value=25, bar_type="increase", color="#00AA00"),
            ],
        )
        render_waterfall_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "waterfall_custom.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_waterfall_no_title(self, tmp_path):
        brand = PRESETS["executive"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.WATERFALL,
            waterfall_bars=[
                WaterfallBar(label="A", value=50, bar_type="total"),
                WaterfallBar(label="B", value=10, bar_type="increase"),
            ],
        )
        render_waterfall_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "waterfall_notitle.pptx"
        prs.save(str(out))
        assert out.exists()


# ── Hub-Spoke ──────────────────────────────────────────────────────────


class TestHubSpoke:
    def test_basic_hub_spoke(self, tmp_path):
        brand = PRESETS["executive"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.HUB_SPOKE,
            title="Core Competencies",
            hub_spoke_center="Strategy",
            spokes=[
                SpokeItem(label="Sales"),
                SpokeItem(label="Marketing"),
                SpokeItem(label="Engineering"),
                SpokeItem(label="Operations"),
            ],
        )
        render_hub_spoke_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "hub_spoke.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_hub_spoke_with_descriptions(self, tmp_path):
        brand = PRESETS["consulting"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.HUB_SPOKE,
            title="Platform",
            hub_spoke_center="Platform",
            spokes=[
                SpokeItem(label="API", description="REST & GraphQL"),
                SpokeItem(label="Auth", description="OAuth 2.0"),
                SpokeItem(label="Data", description="PostgreSQL"),
            ],
        )
        render_hub_spoke_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "hub_spoke_desc.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_hub_spoke_empty(self, tmp_path):
        brand = PRESETS["executive"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.HUB_SPOKE,
            title="Empty Hub",
            hub_spoke_center=None,
            spokes=[],
        )
        render_hub_spoke_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "hub_spoke_empty.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_hub_spoke_many_spokes(self, tmp_path):
        brand = PRESETS["technical"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.HUB_SPOKE,
            title="Many Spokes",
            hub_spoke_center="Core",
            spokes=[SpokeItem(label=f"Spoke {i+1}") for i in range(8)],
        )
        render_hub_spoke_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "hub_spoke_many.pptx"
        prs.save(str(out))
        assert out.exists()


# ── Cycle ──────────────────────────────────────────────────────────────


class TestCycle:
    def test_basic_cycle(self, tmp_path):
        brand = PRESETS["executive"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.CYCLE,
            title="PDCA Cycle",
            cycle_steps=[
                CycleStep(label="Plan"),
                CycleStep(label="Do"),
                CycleStep(label="Check"),
                CycleStep(label="Act"),
            ],
        )
        render_cycle_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "cycle.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_cycle_with_descriptions(self, tmp_path):
        brand = PRESETS["corporate"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.CYCLE,
            title="DevOps Cycle",
            cycle_steps=[
                CycleStep(label="Code", description="Write features"),
                CycleStep(label="Build", description="CI pipeline"),
                CycleStep(label="Test", description="Automated QA"),
                CycleStep(label="Deploy", description="CD to prod"),
                CycleStep(label="Monitor", description="Observability"),
            ],
        )
        render_cycle_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "cycle_desc.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_cycle_empty(self, tmp_path):
        brand = PRESETS["minimal"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.CYCLE,
            title="Empty Cycle",
            cycle_steps=[],
        )
        render_cycle_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "cycle_empty.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_cycle_two_steps(self, tmp_path):
        brand = PRESETS["executive"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.CYCLE,
            title="Two-Step",
            cycle_steps=[
                CycleStep(label="Request"),
                CycleStep(label="Respond"),
            ],
        )
        render_cycle_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "cycle_two.pptx"
        prs.save(str(out))
        assert out.exists()


# ── Gantt ──────────────────────────────────────────────────────────────


class TestGantt:
    def test_basic_gantt(self, tmp_path):
        brand = PRESETS["executive"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.GANTT,
            title="Project Timeline",
            gantt_tasks=[
                GanttTask(label="Research", start=0.0, duration=0.25, group="Phase 1"),
                GanttTask(label="Design", start=0.2, duration=0.3, group="Phase 1"),
                GanttTask(label="Development", start=0.4, duration=0.4, group="Phase 2"),
                GanttTask(label="Testing", start=0.7, duration=0.2, group="Phase 2"),
                GanttTask(label="Launch", start=0.9, duration=0.1, group="Phase 3"),
            ],
            gantt_phases=["Q1", "Q2", "Q3", "Q4"],
        )
        render_gantt_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "gantt.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_gantt_with_progress(self, tmp_path):
        brand = PRESETS["consulting"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.GANTT,
            title="Sprint Progress",
            gantt_tasks=[
                GanttTask(label="Backend API", start=0.0, duration=0.5, progress=0.8),
                GanttTask(label="Frontend", start=0.2, duration=0.6, progress=0.4),
                GanttTask(label="QA", start=0.5, duration=0.3, progress=0.0),
            ],
        )
        render_gantt_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "gantt_progress.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_gantt_empty(self, tmp_path):
        brand = PRESETS["executive"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.GANTT,
            title="Empty Gantt",
            gantt_tasks=[],
        )
        render_gantt_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "gantt_empty.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_gantt_no_phases(self, tmp_path):
        brand = PRESETS["technical"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.GANTT,
            title="No Phases",
            gantt_tasks=[
                GanttTask(label="Task A", start=0.0, duration=0.3),
                GanttTask(label="Task B", start=0.3, duration=0.4),
            ],
        )
        render_gantt_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "gantt_no_phases.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_gantt_many_tasks(self, tmp_path):
        brand = PRESETS["startup"].model_copy(deep=True)
        prs, slide = _make_slide()
        tasks = []
        for i in range(12):
            tasks.append(GanttTask(
                label=f"Task {i+1}",
                start=i * 0.07,
                duration=0.15,
                group=f"Team {'A' if i < 6 else 'B'}",
            ))
        defn = SlideDefinition(
            slide_type=SlideType.GANTT,
            title="Many Tasks",
            gantt_tasks=tasks,
            gantt_phases=["Month 1", "Month 2", "Month 3"],
        )
        render_gantt_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "gantt_many.pptx"
        prs.save(str(out))
        assert out.exists()

    def test_gantt_custom_colors(self, tmp_path):
        brand = PRESETS["executive"].model_copy(deep=True)
        prs, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.GANTT,
            title="Custom Color Gantt",
            gantt_tasks=[
                GanttTask(label="Red Task", start=0.0, duration=0.5, color="#CC0000"),
                GanttTask(label="Blue Task", start=0.3, duration=0.5, color="#0000CC"),
            ],
        )
        render_gantt_slide(slide, defn, brand, SLIDE_WIDTH)
        out = tmp_path / "gantt_custom.pptx"
        prs.save(str(out))
        assert out.exists()
