"""Tests for designer edge rendering: shadow presets, line dash, edge bars, dividers, auto-fit, logo."""

from __future__ import annotations

import os
import struct
import tempfile

import pytest
from pptx import Presentation
from pptx.enum.dml import MSO_LINE_DASH_STYLE
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import MSO_AUTO_SIZE
from pptx.util import Inches, Pt

from pptx_mcp.engine.shapes import (
    _DASH_MAP,
    _SHADOW_PRESETS,
    add_drop_shadow,
    apply_line_dash,
    resolve_shadow_params,
)
from pptx_mcp.engine.layouts import (
    _add_divider,
    _add_edge_bar,
    _apply_auto_fit,
    _style_card,
    render_content_slide,
)
from pptx_mcp.engine.generator import DeckGenerator
from pptx_mcp.models.brand import (
    BrandConfig,
    ColorPalette,
    LogoConfig,
    ShapeStyleConfig,
)
from pptx_mcp.models.slides import (
    PresentationDefinition,
    PresentationMetadata,
    SlideDefinition,
    SlideType,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _brand(**overrides) -> BrandConfig:
    """Return a minimal BrandConfig, applying overrides to ShapeStyleConfig."""
    ss_kwargs = {
        "card_corner_radius": 0.08,
        "card_shadow": False,
    }
    ss_kwargs.update(overrides)
    return BrandConfig(
        name="TestBrand",
        colors=ColorPalette(
            primary="#1B2A4A",
            secondary="#2D5F8A",
            accent="#E8913A",
            border="#E0E0E0",
        ),
        shape_style=ShapeStyleConfig(**ss_kwargs),
    )


def _make_slide() -> tuple[Presentation, object]:
    """Return a Presentation and a blank slide."""
    prs = Presentation()
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    return prs, slide


def _make_rounded_rect(slide) -> object:
    """Add a rounded rectangle shape to the slide."""
    return slide.shapes.add_shape(
        MSO_SHAPE.ROUNDED_RECTANGLE,
        Inches(1), Inches(1), Inches(3), Inches(2),
    )


def _create_test_png(path: str) -> None:
    """Write a minimal valid 1x1 PNG to *path*."""
    # Minimal 1x1 white PNG (67 bytes)
    import zlib

    def _chunk(chunk_type: bytes, data: bytes) -> bytes:
        c = chunk_type + data
        return struct.pack(">I", len(data)) + c + struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF)

    sig = b"\x89PNG\r\n\x1a\n"
    ihdr = _chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0))
    raw = zlib.compress(b"\x00\xff\xff\xff")
    idat = _chunk(b"IDAT", raw)
    iend = _chunk(b"IEND", b"")
    with open(path, "wb") as f:
        f.write(sig + ihdr + idat + iend)


# ---------------------------------------------------------------------------
# resolve_shadow_params
# ---------------------------------------------------------------------------


class TestResolveShadowParams:
    def test_subtle_preset(self):
        brand = _brand(shadow_preset="subtle")
        result = resolve_shadow_params(brand)
        assert result == (2.0, 1.5, 15)

    def test_medium_preset(self):
        brand = _brand(shadow_preset="medium")
        result = resolve_shadow_params(brand)
        assert result == (4.0, 3.0, 25)

    def test_elevated_preset(self):
        brand = _brand(shadow_preset="elevated")
        result = resolve_shadow_params(brand)
        assert result == (6.0, 4.0, 35)

    def test_none_without_card_shadow(self):
        brand = _brand(shadow_preset="none", card_shadow=False)
        assert resolve_shadow_params(brand) is None

    def test_none_with_card_shadow_uses_raw_fields(self):
        brand = _brand(
            shadow_preset="none",
            card_shadow=True,
            shadow_blur_pt=10.0,
            shadow_offset_pt=5.0,
            shadow_opacity_pct=50,
        )
        result = resolve_shadow_params(brand)
        assert result == (10.0, 5.0, 50)

    def test_preset_overrides_card_shadow_flag(self):
        """When preset is set, card_shadow flag is irrelevant."""
        brand = _brand(shadow_preset="medium", card_shadow=False)
        result = resolve_shadow_params(brand)
        assert result == (4.0, 3.0, 25)


# ---------------------------------------------------------------------------
# _DASH_MAP
# ---------------------------------------------------------------------------


class TestDashMap:
    def test_all_entries_present(self):
        expected = {"solid", "dash", "dot", "dash_dot", "long_dash"}
        assert set(_DASH_MAP.keys()) == expected

    def test_solid(self):
        assert _DASH_MAP["solid"] == MSO_LINE_DASH_STYLE.SOLID

    def test_dash(self):
        assert _DASH_MAP["dash"] == MSO_LINE_DASH_STYLE.DASH

    def test_dot(self):
        assert _DASH_MAP["dot"] == MSO_LINE_DASH_STYLE.ROUND_DOT

    def test_dash_dot(self):
        assert _DASH_MAP["dash_dot"] == MSO_LINE_DASH_STYLE.DASH_DOT

    def test_long_dash(self):
        assert _DASH_MAP["long_dash"] == MSO_LINE_DASH_STYLE.LONG_DASH


# ---------------------------------------------------------------------------
# apply_line_dash
# ---------------------------------------------------------------------------


class TestApplyLineDash:
    def test_sets_dash_style(self):
        _, slide = _make_slide()
        shape = _make_rounded_rect(slide)
        apply_line_dash(shape, "dash")
        assert shape.line.dash_style == MSO_LINE_DASH_STYLE.DASH

    def test_unknown_style_no_op(self):
        _, slide = _make_slide()
        shape = _make_rounded_rect(slide)
        # Should not raise
        apply_line_dash(shape, "nonexistent")


# ---------------------------------------------------------------------------
# _add_edge_bar
# ---------------------------------------------------------------------------


class TestAddEdgeBar:
    def test_left_edge_bar(self):
        _, slide = _make_slide()
        brand = _brand(edge_bar="left", edge_bar_width=0.15)
        initial = len(slide.shapes)
        _add_edge_bar(slide, brand, slide_width=13.333)
        assert len(slide.shapes) == initial + 1
        bar = slide.shapes[-1]
        assert bar.left == 0
        assert bar.top == 0
        # Width should be 0.15 inches
        assert abs(bar.width - Inches(0.15)) < Inches(0.01)
        # Height should be full slide height (7.5 inches)
        assert abs(bar.height - Inches(7.5)) < Inches(0.01)

    def test_top_edge_bar(self):
        _, slide = _make_slide()
        brand = _brand(edge_bar="top", edge_bar_width=0.15)
        initial = len(slide.shapes)
        _add_edge_bar(slide, brand, slide_width=13.333)
        assert len(slide.shapes) == initial + 1
        bar = slide.shapes[-1]
        assert bar.left == 0
        assert bar.top == 0
        # Width should span the slide
        assert abs(bar.width - Inches(13.333)) < Inches(0.01)
        # Height should be 0.15 inches
        assert abs(bar.height - Inches(0.15)) < Inches(0.01)

    def test_none_edge_bar_no_shape(self):
        _, slide = _make_slide()
        brand = _brand(edge_bar="none")
        initial = len(slide.shapes)
        _add_edge_bar(slide, brand, slide_width=13.333)
        assert len(slide.shapes) == initial


# ---------------------------------------------------------------------------
# _add_divider
# ---------------------------------------------------------------------------


class TestAddDivider:
    def test_thin_line_horizontal(self):
        _, slide = _make_slide()
        brand = _brand(divider_style="thin_line")
        initial = len(slide.shapes)
        _add_divider(slide, brand, x=1.0, y=3.0, length=5.0)
        assert len(slide.shapes) == initial + 1

    def test_dotted_divider(self):
        _, slide = _make_slide()
        brand = _brand(divider_style="dotted")
        _add_divider(slide, brand, x=1.0, y=3.0, length=5.0)
        shape = slide.shapes[-1]
        assert shape.line.dash_style == MSO_LINE_DASH_STYLE.ROUND_DOT

    def test_vertical_divider(self):
        _, slide = _make_slide()
        brand = _brand(divider_style="thin_line")
        _add_divider(slide, brand, x=5.0, y=1.0, length=4.0, vertical=True)
        shape = slide.shapes[-1]
        # Vertical: width is narrow, height is length
        assert shape.width < Inches(0.1)
        assert abs(shape.height - Inches(4.0)) < Inches(0.01)

    def test_none_divider_no_shape(self):
        _, slide = _make_slide()
        brand = _brand(divider_style="none")
        initial = len(slide.shapes)
        _add_divider(slide, brand, x=1.0, y=3.0, length=5.0)
        assert len(slide.shapes) == initial


# ---------------------------------------------------------------------------
# _style_card with shadow_preset
# ---------------------------------------------------------------------------


class TestStyleCardWithPreset:
    def test_subtle_preset_applies_shadow(self):
        _, slide = _make_slide()
        shape = _make_rounded_rect(slide)
        brand = _brand(shadow_preset="subtle")
        _style_card(shape, brand)
        # Shadow XML should be present
        from pptx.oxml.ns import qn
        effect_lst = shape._element.spPr.findall(qn("a:effectLst"))
        assert len(effect_lst) == 1

    def test_none_preset_no_shadow(self):
        _, slide = _make_slide()
        shape = _make_rounded_rect(slide)
        brand = _brand(shadow_preset="none", card_shadow=False)
        _style_card(shape, brand)
        from pptx.oxml.ns import qn
        effect_lst = shape._element.spPr.findall(qn("a:effectLst"))
        assert len(effect_lst) == 0


# ---------------------------------------------------------------------------
# Text auto-fit
# ---------------------------------------------------------------------------


class TestTextAutoFit:
    def test_auto_fit_enabled(self):
        _, slide = _make_slide()
        brand = _brand(text_auto_fit=True)
        txbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(2))
        tf = txbox.text_frame
        _apply_auto_fit(tf, brand)
        assert tf.auto_size == MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE

    def test_auto_fit_disabled_by_default(self):
        _, slide = _make_slide()
        brand = _brand()  # text_auto_fit defaults to False
        txbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(2))
        tf = txbox.text_frame
        _apply_auto_fit(tf, brand)
        # auto_size should not be set to TEXT_TO_FIT_SHAPE
        assert tf.auto_size != MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE

    def test_auto_fit_on_content_slide(self):
        """Content slide bullet text frame should have auto-fit when enabled."""
        brand = _brand(text_auto_fit=True)
        _, slide = _make_slide()
        defn = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Test Title",
            bullets=["Point A", "Point B"],
        )
        render_content_slide(slide, defn, brand, 13.333)
        # Find the bullet textbox — it's the last text frame with bullets
        found_auto_fit = False
        for shape in slide.shapes:
            if shape.has_text_frame:
                if shape.text_frame.auto_size == MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE:
                    found_auto_fit = True
                    break
        assert found_auto_fit


# ---------------------------------------------------------------------------
# _add_logo
# ---------------------------------------------------------------------------


class TestAddLogo:
    def test_logo_placed_on_content_slide(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            logo_path = os.path.join(tmpdir, "logo.png")
            _create_test_png(logo_path)

            brand = _brand()
            brand.logo = LogoConfig(path=logo_path, placement="top_right")

            defn = SlideDefinition(
                slide_type=SlideType.CONTENT,
                title="Test",
                bullets=["A"],
            )
            prs_def = PresentationDefinition(
                metadata=PresentationMetadata(title="Logo Test"),
                slides=[defn],
            )

            gen = DeckGenerator(brand=brand)
            out = os.path.join(tmpdir, "out.pptx")
            gen.generate(prs_def, out)

            prs = Presentation(out)
            slide = prs.slides[0]
            # Should have a picture shape from the logo
            pic_shapes = [s for s in slide.shapes if s.shape_type == 13]  # MSO_SHAPE_TYPE.PICTURE
            assert len(pic_shapes) >= 1

    def test_logo_skipped_on_title_slide(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            logo_path = os.path.join(tmpdir, "logo.png")
            _create_test_png(logo_path)

            brand = _brand()
            brand.logo = LogoConfig(path=logo_path, skip_on=["title"])

            defn = SlideDefinition(
                slide_type=SlideType.TITLE,
                title="Welcome",
            )
            prs_def = PresentationDefinition(
                metadata=PresentationMetadata(title="Logo Skip Test"),
                slides=[defn],
            )

            gen = DeckGenerator(brand=brand)
            out = os.path.join(tmpdir, "out.pptx")
            gen.generate(prs_def, out)

            prs = Presentation(out)
            slide = prs.slides[0]
            pic_shapes = [s for s in slide.shapes if s.shape_type == 13]
            assert len(pic_shapes) == 0

    def test_logo_no_path_no_error(self):
        """When no logo path is configured, logo placement is silently skipped."""
        brand = _brand()
        defn = SlideDefinition(slide_type=SlideType.CONTENT, title="T", bullets=["a"])
        prs_def = PresentationDefinition(
            metadata=PresentationMetadata(title="No Logo"),
            slides=[defn],
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            gen = DeckGenerator(brand=brand)
            out = os.path.join(tmpdir, "out.pptx")
            gen.generate(prs_def, out)
            assert os.path.exists(out)


# ---------------------------------------------------------------------------
# Full render with edge_bar (startup preset)
# ---------------------------------------------------------------------------


class TestFullRenderEdgeBar:
    def test_startup_preset_left_edge_bar(self):
        """Startup preset has edge_bar='left' — verify it renders."""
        from pptx_mcp.brand.defaults import startup_default

        brand = startup_default()
        defn = SlideDefinition(
            slide_type=SlideType.CONTENT,
            title="Startup Slide",
            bullets=["Scale fast", "Ship often"],
        )
        prs_def = PresentationDefinition(
            metadata=PresentationMetadata(title="Startup Test"),
            slides=[defn],
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            gen = DeckGenerator(brand=brand)
            out = os.path.join(tmpdir, "startup.pptx")
            gen.generate(prs_def, out)

            prs = Presentation(out)
            slide = prs.slides[0]
            # The edge bar should be a shape at x=0, y=0
            edge_bars = [
                s for s in slide.shapes
                if s.left == 0 and s.top == 0 and s.width < Inches(0.3)
            ]
            assert len(edge_bars) >= 1
