"""Tests for SVG chart rendering."""

import os
from datetime import datetime, timedelta
from pathlib import Path
from xml.etree import ElementTree as ET

import pytest

from meshmon.charts import (
    CHART_THEMES,
    DataPoint,
    TimeSeries,
    render_chart_svg,
)

from .conftest import extract_svg_data_attributes, normalize_svg_for_snapshot


def _svg_viewbox_dims(svg: str) -> tuple[float, float]:
    root = ET.fromstring(svg)
    viewbox = root.attrib.get("viewBox")
    assert viewbox is not None
    _, _, width, height = viewbox.split()
    return float(width), float(height)


class TestRenderChartSvg:
    """Tests for render_chart_svg function."""

    def test_returns_svg_string(self, sample_timeseries, light_theme):
        """Returns valid SVG string."""
        svg = render_chart_svg(sample_timeseries, light_theme)

        assert svg.startswith("<?xml") or svg.startswith("<svg")
        assert "</svg>" in svg

    def test_includes_svg_namespace(self, sample_timeseries, light_theme):
        """SVG includes xmlns namespace."""
        svg = render_chart_svg(sample_timeseries, light_theme)

        assert 'xmlns="http://www.w3.org/2000/svg"' in svg

    def test_respects_width_height(self, sample_timeseries, light_theme):
        """SVG respects specified dimensions."""
        svg_default = render_chart_svg(sample_timeseries, light_theme)
        svg_small = render_chart_svg(sample_timeseries, light_theme, width=600, height=200)

        default_w, default_h = _svg_viewbox_dims(svg_default)
        small_w, small_h = _svg_viewbox_dims(svg_small)

        assert small_w < default_w
        assert small_h < default_h

    def test_uses_theme_colors(self, sample_timeseries, light_theme, dark_theme):
        """Different themes produce different colors."""
        light_svg = render_chart_svg(sample_timeseries, light_theme)
        dark_svg = render_chart_svg(sample_timeseries, dark_theme)

        # Check theme colors are present
        assert light_theme.line in light_svg or f"#{light_theme.line}" in light_svg
        assert dark_theme.line in dark_svg or f"#{dark_theme.line}" in dark_svg


class TestEmptyChartRendering:
    """Tests for rendering empty charts."""

    def test_empty_chart_renders(self, empty_timeseries, light_theme):
        """Empty time series renders without error."""
        svg = render_chart_svg(empty_timeseries, light_theme)

        assert "</svg>" in svg

    def test_empty_chart_shows_message(self, empty_timeseries, light_theme):
        """Empty chart shows 'No data available' message."""
        svg = render_chart_svg(empty_timeseries, light_theme)

        assert "No data available" in svg


class TestDataPointsInjection:
    """Tests for data-points attribute injection."""

    def test_includes_data_points(self, sample_timeseries, light_theme):
        """SVG includes data-points attribute."""
        svg = render_chart_svg(sample_timeseries, light_theme)

        assert "data-points=" in svg

    def test_data_points_valid_json(self, sample_timeseries, light_theme):
        """data-points contains valid JSON array."""
        svg = render_chart_svg(sample_timeseries, light_theme)
        data = extract_svg_data_attributes(svg)

        assert "points" in data
        assert isinstance(data["points"], list)

    def test_data_points_count_matches(self, sample_timeseries, light_theme):
        """data-points count matches time series points."""
        svg = render_chart_svg(sample_timeseries, light_theme)
        data = extract_svg_data_attributes(svg)

        assert len(data["points"]) == len(sample_timeseries.points)

    def test_data_points_structure(self, sample_timeseries, light_theme):
        """Each data point has ts and v keys."""
        svg = render_chart_svg(sample_timeseries, light_theme)
        data = extract_svg_data_attributes(svg)

        for point in data["points"]:
            assert "ts" in point
            assert "v" in point
            assert isinstance(point["ts"], int)
            assert isinstance(point["v"], (int, float))

    def test_includes_metadata_attributes(self, sample_timeseries, light_theme):
        """SVG includes metric, period, theme attributes."""
        svg = render_chart_svg(sample_timeseries, light_theme)
        data = extract_svg_data_attributes(svg)

        assert data.get("metric") == "bat"
        assert data.get("period") == "day"
        assert data.get("theme") == "light"

    def test_includes_axis_range_attributes(self, sample_timeseries, light_theme):
        """SVG includes x and y axis range attributes."""
        svg = render_chart_svg(sample_timeseries, light_theme)
        data = extract_svg_data_attributes(svg)

        assert "x_start" in data
        assert "x_end" in data
        assert "y_min" in data
        assert "y_max" in data


class TestYAxisLimits:
    """Tests for Y-axis limit handling."""

    def test_fixed_y_limits(self, sample_timeseries, light_theme):
        """Fixed Y limits are applied."""
        svg = render_chart_svg(
            sample_timeseries, light_theme,
            y_min=3.0, y_max=4.5
        )
        data = extract_svg_data_attributes(svg)

        assert float(data["y_min"]) == 3.0
        assert float(data["y_max"]) == 4.5

    def test_auto_y_limits_with_padding(self, light_theme):
        """Auto Y limits add padding around data."""
        now = datetime.now()
        points = [
            DataPoint(timestamp=now, value=10.0),
            DataPoint(timestamp=now + timedelta(hours=1), value=20.0),
        ]
        ts = TimeSeries(metric="test", role="repeater", period="day", points=points)

        svg = render_chart_svg(ts, light_theme)
        data = extract_svg_data_attributes(svg)

        y_min = float(data["y_min"])
        y_max = float(data["y_max"])

        # Auto limits should extend beyond data range
        assert y_min < 10.0
        assert y_max > 20.0


class TestXAxisLimits:
    """Tests for X-axis limit handling."""

    def test_fixed_x_limits(self, sample_timeseries, light_theme):
        """Fixed X limits are applied."""
        x_start = datetime(2024, 1, 1, 0, 0, 0)
        x_end = datetime(2024, 1, 2, 0, 0, 0)

        svg = render_chart_svg(
            sample_timeseries, light_theme,
            x_start=x_start, x_end=x_end
        )
        data = extract_svg_data_attributes(svg)

        assert int(data["x_start"]) == int(x_start.timestamp())
        assert int(data["x_end"]) == int(x_end.timestamp())


class TestChartThemes:
    """Tests for chart theme constants."""

    def test_light_theme_exists(self):
        """Light theme is defined."""
        assert "light" in CHART_THEMES

    def test_dark_theme_exists(self):
        """Dark theme is defined."""
        assert "dark" in CHART_THEMES

    def test_themes_have_required_colors(self):
        """Themes have all required color attributes."""
        required = ["background", "canvas", "text", "axis", "grid", "line", "area"]

        for theme in CHART_THEMES.values():
            for attr in required:
                assert hasattr(theme, attr), f"Theme missing {attr}"
                assert getattr(theme, attr), f"Theme {attr} is empty"

    def test_theme_colors_are_valid_hex(self):
        """Theme colors are valid hex strings."""
        import re
        hex_pattern = re.compile(r'^[0-9a-fA-F]{6,8}$')

        for name, theme in CHART_THEMES.items():
            for attr in ["background", "canvas", "text", "axis", "grid", "line", "area"]:
                color = getattr(theme, attr)
                assert hex_pattern.match(color), f"{name}.{attr} = {color} is not valid hex"


class TestSvgNormalization:
    """Tests for SVG snapshot normalization helper."""

    def test_normalize_removes_matplotlib_ids(self, sample_timeseries, light_theme):
        """Normalization removes matplotlib-generated IDs."""
        svg = render_chart_svg(sample_timeseries, light_theme)
        normalized = normalize_svg_for_snapshot(svg)

        # Should not have matplotlib's randomized IDs
        import re
        # Look for patterns like id="abc123-def456"
        random_ids = re.findall(r'id="[a-z0-9]+-[0-9a-f]{8,}"', normalized)
        assert len(random_ids) == 0

    def test_normalize_preserves_data_attributes(self, sample_timeseries, light_theme):
        """Normalization preserves data-* attributes."""
        svg = render_chart_svg(sample_timeseries, light_theme)
        normalized = normalize_svg_for_snapshot(svg)

        assert "data-metric=" in normalized
        assert "data-points=" in normalized

    def test_normalize_removes_matplotlib_comment(self, sample_timeseries, light_theme):
        """Normalization removes matplotlib version comment."""
        svg = render_chart_svg(sample_timeseries, light_theme)
        normalized = normalize_svg_for_snapshot(svg)

        assert "Created with matplotlib" not in normalized


class TestSvgSnapshots:
    """Snapshot tests for SVG chart rendering.

    These tests compare rendered SVG output against saved snapshots
    to detect unintended changes in chart appearance.

    To update snapshots, run: UPDATE_SNAPSHOTS=1 pytest tests/charts/test_chart_render.py
    """

    @pytest.fixture
    def update_snapshots(self):
        """Return True if snapshots should be updated."""
        return os.environ.get("UPDATE_SNAPSHOTS", "").lower() in ("1", "true", "yes")

    def _assert_snapshot_match(
        self,
        actual: str,
        snapshot_path: Path,
        update: bool,
    ) -> None:
        """Compare SVG against snapshot, with optional update mode."""
        # Normalize for comparison
        normalized = normalize_svg_for_snapshot(actual)

        if update:
            # Update mode: write normalized SVG to snapshot
            snapshot_path.parent.mkdir(parents=True, exist_ok=True)
            snapshot_path.write_text(normalized, encoding="utf-8")
            pytest.skip(f"Snapshot updated: {snapshot_path}")
        else:
            # Compare mode
            if not snapshot_path.exists():
                # Create new snapshot if it doesn't exist
                snapshot_path.parent.mkdir(parents=True, exist_ok=True)
                snapshot_path.write_text(normalized, encoding="utf-8")
                pytest.fail(
                    f"Snapshot created: {snapshot_path}\n"
                    f"Run tests again to verify, or set UPDATE_SNAPSHOTS=1 to regenerate."
                )

            expected = snapshot_path.read_text(encoding="utf-8")

            if normalized != expected:
                # Show first difference for debugging
                norm_lines = normalized.splitlines()
                exp_lines = expected.splitlines()

                diff_info = []
                for i, (n, e) in enumerate(zip(norm_lines, exp_lines, strict=False), 1):
                    if n != e:
                        diff_info.append(f"Line {i} differs:")
                        diff_info.append(f"  Expected: {e[:100]}...")
                        diff_info.append(f"  Actual:   {n[:100]}...")
                        if len(diff_info) > 12:
                            diff_info.append("  (more differences omitted)")
                            break

                if len(norm_lines) != len(exp_lines):
                    diff_info.append(
                        f"Line count: expected {len(exp_lines)}, got {len(norm_lines)}"
                    )

                pytest.fail(
                    f"Snapshot mismatch: {snapshot_path}\n"
                    f"Set UPDATE_SNAPSHOTS=1 to regenerate.\n\n"
                    + "\n".join(diff_info)
                )

    def test_gauge_chart_light_theme(
        self,
        snapshot_gauge_timeseries,
        light_theme,
        snapshots_dir,
        update_snapshots,
    ):
        """Gauge metric chart with light theme matches snapshot."""
        svg = render_chart_svg(
            snapshot_gauge_timeseries,
            light_theme,
            y_min=3.0,
            y_max=4.2,
        )

        snapshot_path = snapshots_dir / "bat_day_light.svg"
        self._assert_snapshot_match(svg, snapshot_path, update_snapshots)

    def test_gauge_chart_dark_theme(
        self,
        snapshot_gauge_timeseries,
        dark_theme,
        snapshots_dir,
        update_snapshots,
    ):
        """Gauge metric chart with dark theme matches snapshot."""
        svg = render_chart_svg(
            snapshot_gauge_timeseries,
            dark_theme,
            y_min=3.0,
            y_max=4.2,
        )

        snapshot_path = snapshots_dir / "bat_day_dark.svg"
        self._assert_snapshot_match(svg, snapshot_path, update_snapshots)

    def test_counter_chart_light_theme(
        self,
        snapshot_counter_timeseries,
        light_theme,
        snapshots_dir,
        update_snapshots,
    ):
        """Counter metric (rate) chart with light theme matches snapshot."""
        svg = render_chart_svg(
            snapshot_counter_timeseries,
            light_theme,
        )

        snapshot_path = snapshots_dir / "nb_recv_day_light.svg"
        self._assert_snapshot_match(svg, snapshot_path, update_snapshots)

    def test_counter_chart_dark_theme(
        self,
        snapshot_counter_timeseries,
        dark_theme,
        snapshots_dir,
        update_snapshots,
    ):
        """Counter metric (rate) chart with dark theme matches snapshot."""
        svg = render_chart_svg(
            snapshot_counter_timeseries,
            dark_theme,
        )

        snapshot_path = snapshots_dir / "nb_recv_day_dark.svg"
        self._assert_snapshot_match(svg, snapshot_path, update_snapshots)

    def test_empty_chart_light_theme(
        self,
        snapshot_empty_timeseries,
        light_theme,
        snapshots_dir,
        update_snapshots,
    ):
        """Empty chart with 'No data available' matches snapshot."""
        svg = render_chart_svg(
            snapshot_empty_timeseries,
            light_theme,
        )

        snapshot_path = snapshots_dir / "empty_day_light.svg"
        self._assert_snapshot_match(svg, snapshot_path, update_snapshots)

    def test_empty_chart_dark_theme(
        self,
        snapshot_empty_timeseries,
        dark_theme,
        snapshots_dir,
        update_snapshots,
    ):
        """Empty chart with dark theme matches snapshot."""
        svg = render_chart_svg(
            snapshot_empty_timeseries,
            dark_theme,
        )

        snapshot_path = snapshots_dir / "empty_day_dark.svg"
        self._assert_snapshot_match(svg, snapshot_path, update_snapshots)

    def test_single_point_chart(
        self,
        snapshot_single_point_timeseries,
        light_theme,
        snapshots_dir,
        update_snapshots,
    ):
        """Chart with single data point matches snapshot."""
        svg = render_chart_svg(
            snapshot_single_point_timeseries,
            light_theme,
            y_min=3.0,
            y_max=4.2,
        )

        snapshot_path = snapshots_dir / "single_point_day_light.svg"
        self._assert_snapshot_match(svg, snapshot_path, update_snapshots)
