import datetime
from typing import Any
from unittest.mock import MagicMock

import polars as pl
import pytest
from fields_metadata import FieldMetadata
from polychromos import HSLColor

from orama.color import (
    ColorAssignerByCategory,
    ColorAssignerByValue,
    ColorAssignerStrategy,
    ColorRule,
    ColorSelectByCategoryName,
    ColorSelectorStrategy,
    ColorStrategyDescription,
)
from orama.strategies.base import FigureStrategy, FigureStrategyDescription, _hex_to_rgb


class ConcreteStrategy(FigureStrategy):
    @classmethod
    def supported_color_selectors(cls) -> list[type[ColorSelectorStrategy]]:
        return [ColorSelectorStrategy]

    @classmethod
    def supported_color_assigners(cls) -> list[type[ColorAssignerStrategy]]:
        return [ColorAssignerByCategory]

    def get_query_params(self) -> dict[str, Any]:
        return {}

    def _render_from_payload(self, payload: dict[str, Any]) -> Any:
        import plotly.graph_objects as go

        fig = go.Figure()
        if payload.get("title"):
            self._apply_title(fig, payload["title"], payload.get("subtitle"))
        return fig

    def _get_schema_fields(self) -> dict[str, type[pl.DataType]]:
        return {}


def _make_instance(**kwargs: Any) -> ConcreteStrategy:
    return ConcreteStrategy(fields_metadata={}, **kwargs)


def test_init_unsupported_color_selector_raises(anchor_colors: list[HSLColor]) -> None:
    assigner = ColorAssignerByCategory(anchor_colors=anchor_colors)
    unsupported_selector = ColorSelectByCategoryName(["A"])
    rule = ColorRule(assigner=assigner, selector=unsupported_selector)
    with pytest.raises(ValueError):
        ConcreteStrategy(fields_metadata={}, color_rules=[rule])


def test_init_unsupported_color_assigner_raises(anchor_colors: list[HSLColor]) -> None:
    unsupported_assigner = ColorAssignerByValue(anchor_colors=anchor_colors)
    rule = ColorRule(assigner=unsupported_assigner)
    with pytest.raises(ValueError):
        ConcreteStrategy(fields_metadata={}, color_rules=[rule])


def test_init_supported_rules_accepted(anchor_colors: list[HSLColor]) -> None:
    assigner = ColorAssignerByCategory(anchor_colors=anchor_colors)
    rule = ColorRule(assigner=assigner, selector=ColorSelectorStrategy())
    ConcreteStrategy(fields_metadata={}, color_rules=[rule])


def test_title_property() -> None:
    instance = _make_instance(title="My Title")
    assert instance.title == "My Title"


def test_subtitle_property() -> None:
    instance = _make_instance(subtitle="My Subtitle")
    assert instance.subtitle == "My Subtitle"


def test_caption_property() -> None:
    instance = _make_instance(caption="My Caption")
    assert instance.caption == "My Caption"


def test_storytelling_context_property() -> None:
    instance = _make_instance(storytelling_context="Revenue grew 3x after the product launch.")
    assert instance.storytelling_context == "Revenue grew 3x after the product launch."


def test_storytelling_context_default_none() -> None:
    instance = _make_instance()
    assert instance.storytelling_context is None


def test_properties_default_none() -> None:
    instance = _make_instance()
    assert instance.title is None
    assert instance.subtitle is None
    assert instance.caption is None
    assert instance.storytelling_context is None


@pytest.mark.parametrize(
    "hex_color,expected",
    [
        ("#ff0000", (255, 0, 0)),
        ("#00ff00", (0, 255, 0)),
        ("#0000ff", (0, 0, 255)),
        ("#000000", (0, 0, 0)),
        ("#ffffff", (255, 255, 255)),
    ],
)
def test_hex_to_rgb(hex_color: str, expected: tuple[int, int, int]) -> None:
    assert _hex_to_rgb(hex_color) == expected


def _make_meta(effective_type: type) -> MagicMock:
    mock = MagicMock(spec=FieldMetadata)
    mock.effective_type = effective_type
    return mock


@pytest.mark.parametrize(
    "python_type,expected_polars_type",
    [
        (int, pl.Int64),
        (float, pl.Float64),
        (str, pl.Utf8),
        (bool, pl.Boolean),
        (datetime.datetime, pl.Datetime),
        (datetime.date, pl.Datetime),
        (list, pl.Utf8),
    ],
)
def test_infer_column_type(python_type: type, expected_polars_type: type) -> None:
    instance = _make_instance()
    result = instance._infer_column_type({"col": _make_meta(python_type)}, "col")
    assert result == expected_polars_type


def test_infer_column_type_missing_field_raises() -> None:
    instance = _make_instance()
    with pytest.raises(ValueError):
        instance._infer_column_type({}, "missing_field")


def test_concatenate_variables_empty_list_raises() -> None:
    instance = _make_instance()
    df = pl.DataFrame({"col": [1, 2, 3]})
    with pytest.raises(ValueError):
        instance._concatenate_variables(df, [])


def test_concatenate_variables_single_with_alias() -> None:
    instance = _make_instance()
    df = pl.DataFrame({"col1": ["a", "b", "c"]})
    result_df, alias = instance._concatenate_variables(df, ["col1"], "my_alias")
    assert alias == "my_alias"
    assert "my_alias" in result_df.columns
    assert "col1" not in result_df.columns


def test_concatenate_variables_multiple_with_alias() -> None:
    instance = _make_instance()
    df = pl.DataFrame({"col1": ["a", "b"], "col2": ["x", "y"]})
    result_df, alias = instance._concatenate_variables(df, ["col1", "col2"], "combined")
    assert alias == "combined"
    assert "combined" in result_df.columns
    assert result_df["combined"][0] == "a - x"


def test_concatenate_variables_auto_alias_single() -> None:
    instance = _make_instance()
    df = pl.DataFrame({"col1": ["a", "b"]})
    result_df, alias = instance._concatenate_variables(df, ["col1"])
    assert alias == "col1"


def test_concatenate_variables_auto_alias_multiple() -> None:
    instance = _make_instance()
    df = pl.DataFrame({"col1": ["a", "b"], "col2": ["x", "y"]})
    result_df, alias = instance._concatenate_variables(df, ["col1", "col2"])
    assert alias == "col1 - col2"


def test_describe_returns_figure_strategy_description(categorical_metadata: MagicMock) -> None:
    desc = ConcreteStrategy.describe({"field": categorical_metadata})
    assert isinstance(desc, FigureStrategyDescription)
    assert desc.name == "ConcreteStrategy"
    assert desc.variables == {}
    assert desc.options == {}
    assert isinstance(desc.colors, ColorStrategyDescription)


def test_temporal_mapping_month(month_metadata: MagicMock, tr: Any) -> None:
    instance = _make_instance()
    result = instance._get_temporal_label_mapping(month_metadata, tr)
    assert result is not None
    assert len(result) == 12
    assert result[1] == "January"
    assert result[12] == "December"


def test_temporal_mapping_dow(dow_metadata: MagicMock, tr: Any) -> None:
    instance = _make_instance()
    result = instance._get_temporal_label_mapping(dow_metadata, tr)
    assert result is not None
    assert len(result) == 7
    assert result[1] == "Monday"
    assert result[7] == "Sunday"


def test_temporal_mapping_year(tr: Any) -> None:
    meta = MagicMock(spec=FieldMetadata)
    meta.extra = {"suggested_validation": "year"}
    instance = _make_instance()
    result = instance._get_temporal_label_mapping(meta, tr)
    assert result is None


def test_temporal_mapping_no_validation(tr: Any) -> None:
    meta = MagicMock(spec=FieldMetadata)
    meta.extra = {}
    instance = _make_instance()
    result = instance._get_temporal_label_mapping(meta, tr)
    assert result is None


def test_temporal_mapping_none_extra(tr: Any) -> None:
    meta = MagicMock(spec=FieldMetadata)
    meta.extra = None
    instance = _make_instance()
    result = instance._get_temporal_label_mapping(meta, tr)
    assert result is None


def _make_strategy_with_rule(anchor_colors: list[HSLColor]) -> ConcreteStrategy:
    assigner = ColorAssignerByCategory(anchor_colors=anchor_colors)
    rule = ColorRule(assigner=assigner, selector=ColorSelectorStrategy())
    return ConcreteStrategy(fields_metadata={}, color_rules=[rule])


def test_apply_color_rules_length_matches_df_height(
    anchor_colors: list[HSLColor],
    theme: Any,
) -> None:
    import polars as pl

    instance = _make_strategy_with_rule(anchor_colors)
    df = pl.DataFrame({"cat": ["A", "B", "C"], "val": [1, 2, 3]})
    result = instance._apply_color_rules(df, "cat", "val", theme)
    assert len(result) == df.height


def test_apply_color_rules_all_selector_applies_assigner(
    anchor_colors: list[HSLColor],
    theme: Any,
) -> None:
    import polars as pl

    instance = _make_strategy_with_rule(anchor_colors)
    df = pl.DataFrame({"cat": ["A", "B"], "val": [1, 2]})
    result = instance._apply_color_rules(df, "cat", "val", theme)
    assert len(result) == 2
