"""Unit tests for the charts module. No network or MCP server required."""

from __future__ import annotations

import pytest
from mcp.server.fastmcp.utilities.types import Image

from kapsarc_mcp_server.charts import (
    _classify_field,
    _classify_fields,
    _detect_chart_type,
    _auto_select_fields,
    _clean_records,
    _group_excess_categories,
    render_chart,
)

# ---------------------------------------------------------------------------
# Test data factories
# ---------------------------------------------------------------------------


def _time_series(n: int = 10) -> list[dict]:
    return [{"year": 2010 + i, "value": 100 + i * 5} for i in range(n)]


def _categorical_few() -> list[dict]:
    return [
        {"country": "Saudi Arabia", "production": 10.5},
        {"country": "USA", "production": 12.1},
        {"country": "Russia", "production": 10.8},
        {"country": "China", "production": 4.9},
    ]


def _categorical_many() -> list[dict]:
    # Needs >15 unique categories but some repeats so it's not classified as "id"
    records = [{"category": f"Cat_{i:02d}", "value": i * 10} for i in range(20)]
    # Add duplicates to prevent id classification
    records.extend([{"category": "Cat_00", "value": 5}, {"category": "Cat_01", "value": 15}])
    return records


def _proportions() -> list[dict]:
    return [
        {"sector": "Oil", "share": 45.0},
        {"sector": "Gas", "share": 30.0},
        {"sector": "Solar", "share": 15.0},
        {"sector": "Wind", "share": 10.0},
    ]


def _two_numerics() -> list[dict]:
    return [{"gdp": i * 1000, "emissions": i * 50 + 10} for i in range(1, 15)]


def _multi_series() -> list[dict]:
    records = []
    for country in ["SA", "US", "RU"]:
        for y in range(2015, 2021):
            records.append({"year": y, "production": y - 2010, "country": country})
    return records


# ===========================================================================
# 1. Field classification
# ===========================================================================


class TestFieldClassification:
    def test_temporal_by_name(self):
        assert _classify_field("year", [2020, 2021, 2022]) == "temporal"

    def test_temporal_by_keyword_date(self):
        assert _classify_field("start_date", ["2020-01-01", "2021-01-01"]) == "temporal"

    def test_temporal_by_int_range(self):
        assert _classify_field("period_code", [1990, 2000, 2010]) == "temporal"

    def test_numeric(self):
        assert _classify_field("value", [1.5, 2.7, 3.9]) == "numeric"

    def test_numeric_strings(self):
        assert _classify_field("amount", ["100", "200", "300"]) == "numeric"

    def test_categorical(self):
        assert _classify_field("country", ["SA", "US", "RU", "CN"]) == "categorical"

    def test_id_field(self):
        vals = [f"id_{i}" for i in range(10)]
        assert _classify_field("record_id", vals) == "id"

    def test_empty_values(self):
        assert _classify_field("empty", [None, None, ""]) == "categorical"

    def test_mixed_numeric_string(self):
        assert _classify_field("code", ["abc", "123", "def"]) == "categorical"


# ===========================================================================
# 2. Auto-detection
# ===========================================================================


class TestAutoDetection:
    def test_time_series_detects_line(self):
        records = _time_series()
        cls = _classify_fields(records)
        x, y, s = _auto_select_fields(records, cls, None, None, None)
        ct = _detect_chart_type(records, x, y, s, cls)
        assert ct == "line"

    def test_few_categories_detects_bar(self):
        records = _categorical_few()
        cls = _classify_fields(records)
        x, y, s = _auto_select_fields(records, cls, None, None, None)
        ct = _detect_chart_type(records, x, y, s, cls)
        assert ct == "bar"

    def test_many_categories_detects_hbar(self):
        records = _categorical_many()
        cls = _classify_fields(records)
        x, y, s = _auto_select_fields(records, cls, None, None, None)
        ct = _detect_chart_type(records, x, y, s, cls)
        assert ct == "hbar"

    def test_proportions_detects_pie(self):
        records = _proportions()
        cls = _classify_fields(records)
        ct = _detect_chart_type(records, "sector", "share", None, cls)
        assert ct == "pie"

    def test_two_numerics_detects_scatter(self):
        records = _two_numerics()
        cls = _classify_fields(records)
        ct = _detect_chart_type(records, "gdp", "emissions", None, cls)
        assert ct == "scatter"

    def test_series_temporal_detects_multi_line(self):
        records = _multi_series()
        cls = _classify_fields(records)
        ct = _detect_chart_type(records, "year", "production", "country", cls)
        assert ct == "multi_line"

    def test_series_categorical_detects_multi_bar(self):
        records = [
            {"region": "East", "product": "Oil", "sales": 100},
            {"region": "West", "product": "Oil", "sales": 80},
            {"region": "East", "product": "Gas", "sales": 60},
            {"region": "West", "product": "Gas", "sales": 70},
        ]
        cls = _classify_fields(records)
        ct = _detect_chart_type(records, "region", "sales", "product", cls)
        assert ct == "multi_bar"

    def test_explicit_override(self):
        records = _time_series()
        img = render_chart(records, chart_type="bar")
        assert isinstance(img, Image)


# ===========================================================================
# 3. render_chart returns valid Image for each chart type
# ===========================================================================


class TestRenderChart:
    def test_bar_chart(self):
        img = render_chart(_categorical_few(), chart_type="bar")
        assert isinstance(img, Image)
        assert len(img.data) > 100
        assert len(img.data) < 1_000_000

    def test_hbar_chart(self):
        img = render_chart(_categorical_many(), chart_type="hbar")
        assert isinstance(img, Image)
        assert len(img.data) > 100

    def test_line_chart(self):
        img = render_chart(_time_series(), chart_type="line")
        assert isinstance(img, Image)
        assert len(img.data) > 100

    def test_pie_chart(self):
        img = render_chart(_proportions(), chart_type="pie")
        assert isinstance(img, Image)
        assert len(img.data) > 100

    def test_scatter_chart(self):
        img = render_chart(
            _two_numerics(),
            x_field="gdp",
            y_field="emissions",
            chart_type="scatter",
        )
        assert isinstance(img, Image)
        assert len(img.data) > 100

    def test_multi_line_chart(self):
        img = render_chart(
            _multi_series(),
            x_field="year",
            y_field="production",
            series_field="country",
            chart_type="multi_line",
        )
        assert isinstance(img, Image)
        assert len(img.data) > 100

    def test_multi_bar_chart(self):
        records = [
            {"region": "East", "product": "Oil", "sales": 100},
            {"region": "West", "product": "Oil", "sales": 80},
            {"region": "East", "product": "Gas", "sales": 60},
            {"region": "West", "product": "Gas", "sales": 70},
        ]
        img = render_chart(
            records,
            x_field="region",
            y_field="sales",
            series_field="product",
            chart_type="multi_bar",
        )
        assert isinstance(img, Image)
        assert len(img.data) > 100

    def test_auto_detection(self):
        img = render_chart(_time_series())
        assert isinstance(img, Image)

    def test_custom_title(self):
        img = render_chart(_time_series(), title="My Custom Title")
        assert isinstance(img, Image)


# ===========================================================================
# 4. Edge cases
# ===========================================================================


class TestEdgeCases:
    def test_empty_records_raises(self):
        with pytest.raises(ValueError, match="No data"):
            render_chart([])

    def test_single_record(self):
        img = render_chart([{"category": "Oil", "value": 42}])
        assert isinstance(img, Image)

    def test_all_null_y_raises(self):
        records = [
            {"country": "SA", "value": None},
            {"country": "US", "value": None},
        ]
        with pytest.raises(ValueError, match="null"):
            render_chart(records, x_field="country", y_field="value")

    def test_many_categories_grouped(self):
        records = [{"cat": f"Category_{i}", "val": i} for i in range(30)]
        img = render_chart(records, x_field="cat", y_field="val", chart_type="bar")
        assert isinstance(img, Image)

    def test_negative_values_pie_fallback(self):
        records = [
            {"item": "A", "change": 10},
            {"item": "B", "change": -5},
            {"item": "C", "change": 15},
        ]
        # Requesting pie with negatives should not crash (falls back to bar)
        img = render_chart(records, x_field="item", y_field="change", chart_type="pie")
        assert isinstance(img, Image)


# ===========================================================================
# 5. Data cleaning
# ===========================================================================


class TestDataCleaning:
    def test_null_x_dropped(self):
        records = [
            {"year": 2020, "value": 10},
            {"year": None, "value": 20},
            {"year": 2022, "value": 30},
        ]
        cls = _classify_fields(records)
        cleaned = _clean_records(records, "year", "value", cls)
        assert len(cleaned) == 2

    def test_null_y_dropped(self):
        records = [
            {"country": "SA", "value": 10},
            {"country": "US", "value": None},
        ]
        cls = _classify_fields(records)
        cleaned = _clean_records(records, "country", "value", cls)
        assert len(cleaned) == 1

    def test_y_coerced_to_float(self):
        records = [{"country": "SA", "value": "42.5"}]
        cls = _classify_fields(records)
        cleaned = _clean_records(records, "country", "value", cls)
        assert cleaned[0]["value"] == 42.5

    def test_temporal_sorting(self):
        records = [
            {"year": 2022, "value": 1},
            {"year": 2020, "value": 2},
            {"year": 2021, "value": 3},
        ]
        cls = _classify_fields(records)
        cleaned = _clean_records(records, "year", "value", cls)
        years = [r["year"] for r in cleaned]
        assert years == [2020, 2021, 2022]

    def test_long_labels_truncated(self):
        long_name = "A" * 50
        records = [{"category": long_name, "value": 10}]
        cls = {"category": "categorical", "value": "numeric"}
        cleaned = _clean_records(records, "category", "value", cls)
        assert len(cleaned[0]["category"]) <= 26  # 25 + ellipsis


class TestGroupExcessCategories:
    def test_under_limit_unchanged(self):
        x = ["A", "B", "C"]
        y = [1.0, 2.0, 3.0]
        rx, ry = _group_excess_categories(x, y, max_cats=5)
        assert rx == x
        assert ry == y

    def test_over_limit_grouped(self):
        x = [f"Cat_{i}" for i in range(10)]
        y = [float(i) for i in range(10)]
        rx, ry = _group_excess_categories(x, y, max_cats=3)
        assert len(rx) == 4  # 3 + "Other"
        assert rx[-1] == "Other"
        assert sum(ry) == pytest.approx(sum(y))
