"""Tests for basic grouping classes: AggregationFunction, Aggregation, and GroupSpec."""

import pytest

from therismos.grouping import Aggregation, AggregationFunction, GroupSpec


class TestAggregationFunction:
    """Tests for AggregationFunction enumeration."""

    def test_aggregation_function_values(self):
        """Test that AggregationFunction has correct string values."""
        assert AggregationFunction.COUNT == "count"
        assert AggregationFunction.MIN == "min"
        assert AggregationFunction.MAX == "max"
        assert AggregationFunction.AVERAGE == "average"
        assert AggregationFunction.STDDEV == "stddev"
        assert AggregationFunction.MEDIAN == "median"
        assert AggregationFunction.Q1 == "q1"
        assert AggregationFunction.Q3 == "q3"
        assert AggregationFunction.P01 == "p01"
        assert AggregationFunction.P05 == "p05"
        assert AggregationFunction.P10 == "p10"
        assert AggregationFunction.P90 == "p90"
        assert AggregationFunction.P95 == "p95"
        assert AggregationFunction.P99 == "p99"

    def test_requires_field_property(self):
        """Test the requires_field property for all functions."""
        # COUNT does not require a field
        assert not AggregationFunction.COUNT.requires_field

        # All other functions require a field
        assert AggregationFunction.MIN.requires_field
        assert AggregationFunction.MAX.requires_field
        assert AggregationFunction.AVERAGE.requires_field
        assert AggregationFunction.STDDEV.requires_field
        assert AggregationFunction.MEDIAN.requires_field
        assert AggregationFunction.Q1.requires_field
        assert AggregationFunction.Q3.requires_field
        assert AggregationFunction.P01.requires_field
        assert AggregationFunction.P05.requires_field
        assert AggregationFunction.P10.requires_field
        assert AggregationFunction.P90.requires_field
        assert AggregationFunction.P95.requires_field
        assert AggregationFunction.P99.requires_field


class TestAggregation:
    """Tests for Aggregation class."""

    def test_create_count_aggregation(self):
        """Test creating a COUNT aggregation (no field required)."""
        agg = Aggregation("total", AggregationFunction.COUNT)

        assert agg.id == "total"
        assert agg.function == AggregationFunction.COUNT
        assert agg.field is None

    def test_create_min_aggregation(self):
        """Test creating a MIN aggregation with field."""
        agg = Aggregation("min_price", AggregationFunction.MIN, "price")

        assert agg.id == "min_price"
        assert agg.function == AggregationFunction.MIN
        assert agg.field == "price"

    @pytest.mark.parametrize(
        "func,field",
        [
            (AggregationFunction.SUM, "revenue"),
            (AggregationFunction.MAX, "price"),
            (AggregationFunction.AVERAGE, "age"),
            (AggregationFunction.STDDEV, "score"),
            (AggregationFunction.MEDIAN, "value"),
            (AggregationFunction.Q1, "salary"),
            (AggregationFunction.Q3, "salary"),
            (AggregationFunction.P01, "response_time"),
            (AggregationFunction.P99, "response_time"),
        ],
    )
    def test_create_various_aggregations(self, func, field):
        """Test creating various aggregation types with fields."""
        agg = Aggregation("test_agg", func, field)

        assert agg.id == "test_agg"
        assert agg.function == func
        assert agg.field == field

    def test_aggregation_is_frozen(self):
        """Test that Aggregation is immutable (frozen dataclass)."""
        agg = Aggregation("total", AggregationFunction.COUNT)

        with pytest.raises(AttributeError):
            agg.function = AggregationFunction.MIN  # type: ignore

    def test_count_with_field_raises_error(self):
        """Test that COUNT aggregation with a field raises ValueError."""
        with pytest.raises(ValueError, match="COUNT aggregation must not have a field"):
            Aggregation("total", AggregationFunction.COUNT, "price")

    def test_non_count_without_field_raises_error(self):
        """Test that non-COUNT aggregation without field raises ValueError."""
        with pytest.raises(ValueError, match="min aggregation requires a field"):
            Aggregation("min_price", AggregationFunction.MIN)

    def test_aggregation_str_count(self):
        """Test string representation of COUNT aggregation."""
        agg = Aggregation("total", AggregationFunction.COUNT)
        assert str(agg) == "total:count"

    def test_aggregation_str_with_field(self):
        """Test string representation of aggregation with field."""
        agg = Aggregation("avg_price", AggregationFunction.AVERAGE, "price")
        assert str(agg) == "avg_price:average:price"

    def test_has_field_property(self):
        """Test the has_field property."""
        count_agg = Aggregation("total", AggregationFunction.COUNT)
        min_agg = Aggregation("min_price", AggregationFunction.MIN, "price")

        assert not count_agg.has_field
        assert min_agg.has_field


class TestGroupSpec:
    """Tests for GroupSpec class."""

    def test_create_group_spec_with_tuple(self):
        """Test creating a GroupSpec with tuple for group_by."""
        agg = Aggregation("total", AggregationFunction.COUNT)
        spec = GroupSpec(
            group_by=("category", "region"),
            aggregations=[agg],
        )

        assert spec.group_by == ("category", "region")
        assert "total" in spec.aggregations
        assert spec.aggregations["total"] is agg

    def test_create_group_spec_with_list(self):
        """Test creating a GroupSpec with list for group_by (converts to tuple)."""
        agg = Aggregation("total", AggregationFunction.COUNT)
        spec = GroupSpec(
            group_by=["category", "region"],
            aggregations=[agg],
        )

        assert spec.group_by == ("category", "region")
        assert isinstance(spec.group_by, tuple)

    def test_create_group_spec_with_aggregation_list(self):
        """Test creating a GroupSpec with list of aggregations."""
        agg1 = Aggregation("total", AggregationFunction.COUNT)
        agg2 = Aggregation("min_price", AggregationFunction.MIN, "price")

        spec = GroupSpec(
            group_by=["category"],
            aggregations=[agg1, agg2],
        )

        assert len(spec.aggregations) == 2
        assert spec.aggregations["total"] is agg1
        assert spec.aggregations["min_price"] is agg2

    def test_create_group_spec_with_aggregation_dict(self):
        """Test creating a GroupSpec with dict of aggregations."""
        agg1 = Aggregation("total", AggregationFunction.COUNT)
        agg2 = Aggregation("min_price", AggregationFunction.MIN, "price")

        spec = GroupSpec(
            group_by=["category"],
            aggregations={"total": agg1, "min_price": agg2},
        )

        assert len(spec.aggregations) == 2
        assert spec.aggregations["total"] is agg1
        assert spec.aggregations["min_price"] is agg2

    def test_group_spec_is_frozen(self):
        """Test that GroupSpec is immutable (frozen dataclass)."""
        spec = GroupSpec(
            group_by=["category"],
            aggregations=[Aggregation("total", AggregationFunction.COUNT)],
        )

        with pytest.raises(AttributeError):
            spec.group_by = ("region",)  # type: ignore

    def test_aggregation_dict_key_mismatch_raises_error(self):
        """Test that mismatched aggregation dict keys raise ValueError."""
        agg = Aggregation("total", AggregationFunction.COUNT)

        with pytest.raises(ValueError, match="does not match aggregation ID"):
            GroupSpec(
                group_by=["category"],
                aggregations={"wrong_key": agg},
            )

    def test_group_spec_str(self):
        """Test string representation of GroupSpec."""
        spec = GroupSpec(
            group_by=["category", "region"],
            aggregations=[
                Aggregation("total", AggregationFunction.COUNT),
                Aggregation("min_price", AggregationFunction.MIN, "price"),
            ],
        )

        result = str(spec)
        assert result == '("category,region", "total:count,min_price:min:price")'

    def test_group_spec_empty_group_by(self):
        """Test GroupSpec with no grouping fields."""
        spec = GroupSpec(
            group_by=[],
            aggregations=[Aggregation("total", AggregationFunction.COUNT)],
        )

        assert spec.group_by == ()
        assert str(spec) == '("", "total:count")'

    def test_group_spec_empty_aggregations(self):
        """Test GroupSpec with no aggregations."""
        spec = GroupSpec(
            group_by=["category"],
            aggregations=[],
        )

        assert len(spec.aggregations) == 0
        assert str(spec) == '("category", "")'

    def test_group_spec_complex(self):
        """Test GroupSpec with multiple grouping fields and aggregations."""
        spec = GroupSpec(
            group_by=["category", "region", "status"],
            aggregations=[
                Aggregation("count", AggregationFunction.COUNT),
                Aggregation("min_price", AggregationFunction.MIN, "price"),
                Aggregation("max_price", AggregationFunction.MAX, "price"),
                Aggregation("avg_price", AggregationFunction.AVERAGE, "price"),
                Aggregation("p95_latency", AggregationFunction.P95, "latency"),
            ],
        )

        assert len(spec.group_by) == 3
        assert len(spec.aggregations) == 5
        assert "count" in spec.aggregations
        assert "p95_latency" in spec.aggregations
