"""
Deep behavioral tests for data aggregation patterns.
Tests counting, summing, averaging, grouping,
and statistical calculation logic.
"""

from datetime import datetime, timezone, timedelta
from collections import defaultdict
import math


# --- Basic aggregation patterns ---


class TestCountAggregation:
    """Tests for count aggregation patterns."""

    def _count_items(self, items):
        """Count total items."""
        return len(items)

    def _count_by_field(self, items, field):
        """Count items grouped by field."""
        counts = defaultdict(int)
        for item in items:
            key = item.get(field)
            counts[key] += 1
        return dict(counts)

    def _count_if(self, items, predicate):
        """Count items matching predicate."""
        return sum(1 for item in items if predicate(item))

    def test_count_all(self):
        items = [1, 2, 3, 4, 5]
        assert self._count_items(items) == 5

    def test_count_empty(self):
        assert self._count_items([]) == 0

    def test_count_by_field(self):
        items = [
            {"status": "active"},
            {"status": "inactive"},
            {"status": "active"},
        ]
        counts = self._count_by_field(items, "status")
        assert counts["active"] == 2
        assert counts["inactive"] == 1

    def test_count_if(self):
        items = [{"score": 80}, {"score": 45}, {"score": 90}]
        passing = self._count_if(items, lambda x: x["score"] >= 50)
        assert passing == 2


class TestSumAggregation:
    """Tests for sum aggregation patterns."""

    def _sum_field(self, items, field):
        """Sum values of a field."""
        return sum(item.get(field, 0) for item in items)

    def _sum_by_group(self, items, group_field, value_field):
        """Sum values grouped by field."""
        sums = defaultdict(float)
        for item in items:
            key = item.get(group_field)
            sums[key] += item.get(value_field, 0)
        return dict(sums)

    def test_sum_field(self):
        items = [{"amount": 100}, {"amount": 200}, {"amount": 50}]
        assert self._sum_field(items, "amount") == 350

    def test_sum_empty(self):
        assert self._sum_field([], "amount") == 0

    def test_sum_by_group(self):
        items = [
            {"category": "A", "amount": 100},
            {"category": "B", "amount": 200},
            {"category": "A", "amount": 50},
        ]
        sums = self._sum_by_group(items, "category", "amount")
        assert sums["A"] == 150
        assert sums["B"] == 200


class TestAverageAggregation:
    """Tests for average aggregation patterns."""

    def _average_field(self, items, field):
        """Calculate average of a field."""
        if not items:
            return 0
        values = [item.get(field, 0) for item in items]
        return sum(values) / len(values)

    def _weighted_average(self, items, value_field, weight_field):
        """Calculate weighted average."""
        total_value = 0
        total_weight = 0
        for item in items:
            value = item.get(value_field, 0)
            weight = item.get(weight_field, 1)
            total_value += value * weight
            total_weight += weight
        if total_weight == 0:
            return 0
        return total_value / total_weight

    def test_average(self):
        items = [{"score": 80}, {"score": 90}, {"score": 100}]
        assert self._average_field(items, "score") == 90

    def test_average_empty(self):
        assert self._average_field([], "score") == 0

    def test_weighted_average(self):
        items = [
            {"score": 80, "weight": 1},
            {"score": 100, "weight": 2},
        ]
        # (80*1 + 100*2) / (1+2) = 280/3 ≈ 93.33
        result = self._weighted_average(items, "score", "weight")
        assert abs(result - 93.33) < 0.01


# --- Min/Max aggregation patterns ---


class TestMinMaxAggregation:
    """Tests for min/max aggregation patterns."""

    def _min_field(self, items, field, default=None):
        """Get minimum value of a field."""
        if not items:
            return default
        return min(
            item.get(field) for item in items if item.get(field) is not None
        )

    def _max_field(self, items, field, default=None):
        """Get maximum value of a field."""
        if not items:
            return default
        return max(
            item.get(field) for item in items if item.get(field) is not None
        )

    def _find_min_item(self, items, field):
        """Find item with minimum field value."""
        if not items:
            return None
        return min(items, key=lambda x: x.get(field, float("inf")))

    def _find_max_item(self, items, field):
        """Find item with maximum field value."""
        if not items:
            return None
        return max(items, key=lambda x: x.get(field, float("-inf")))

    def test_min_field(self):
        items = [{"value": 50}, {"value": 25}, {"value": 75}]
        assert self._min_field(items, "value") == 25

    def test_max_field(self):
        items = [{"value": 50}, {"value": 25}, {"value": 75}]
        assert self._max_field(items, "value") == 75

    def test_min_empty(self):
        assert self._min_field([], "value", default=-1) == -1

    def test_find_min_item(self):
        items = [{"id": 1, "value": 50}, {"id": 2, "value": 25}]
        result = self._find_min_item(items, "value")
        assert result["id"] == 2


# --- Grouping patterns ---


class TestGroupingAggregation:
    """Tests for grouping aggregation patterns."""

    def _group_by(self, items, field):
        """Group items by field value."""
        groups = defaultdict(list)
        for item in items:
            key = item.get(field)
            groups[key].append(item)
        return dict(groups)

    def _group_by_multiple(self, items, fields):
        """Group by multiple fields."""
        groups = defaultdict(list)
        for item in items:
            key = tuple(item.get(f) for f in fields)
            groups[key].append(item)
        return dict(groups)

    def test_group_by_single(self):
        items = [
            {"category": "A", "value": 1},
            {"category": "B", "value": 2},
            {"category": "A", "value": 3},
        ]
        groups = self._group_by(items, "category")
        assert len(groups["A"]) == 2
        assert len(groups["B"]) == 1

    def test_group_by_multiple(self):
        items = [
            {"category": "A", "status": "active", "value": 1},
            {"category": "A", "status": "inactive", "value": 2},
            {"category": "A", "status": "active", "value": 3},
        ]
        groups = self._group_by_multiple(items, ["category", "status"])
        assert len(groups[("A", "active")]) == 2


# --- Time-based aggregation patterns ---


class TestTimeAggregation:
    """Tests for time-based aggregation patterns."""

    def _group_by_date(self, items, timestamp_field):
        """Group items by date."""
        groups = defaultdict(list)
        for item in items:
            ts = item.get(timestamp_field)
            if ts:
                date_key = ts.date() if hasattr(ts, "date") else ts
                groups[date_key].append(item)
        return dict(groups)

    def _group_by_hour(self, items, timestamp_field):
        """Group items by hour."""
        groups = defaultdict(list)
        for item in items:
            ts = item.get(timestamp_field)
            if ts and hasattr(ts, "hour"):
                hour_key = ts.replace(minute=0, second=0, microsecond=0)
                groups[hour_key].append(item)
        return dict(groups)

    def _count_by_period(self, items, timestamp_field, period_minutes=60):
        """Count items by time period."""
        counts = defaultdict(int)
        for item in items:
            ts = item.get(timestamp_field)
            if ts:
                period = ts.timestamp() // (period_minutes * 60)
                counts[period] += 1
        return dict(counts)

    def test_group_by_date(self):
        now = datetime.now(timezone.utc)
        yesterday = now - timedelta(days=1)
        items = [
            {"timestamp": now},
            {"timestamp": yesterday},
            {"timestamp": now},
        ]
        groups = self._group_by_date(items, "timestamp")
        assert len(groups) == 2

    def test_count_by_period(self):
        # Use a base time well inside a period boundary to avoid floating point edge cases
        base = datetime(2024, 1, 1, 12, 15, 0, tzinfo=timezone.utc)
        items = [
            {"timestamp": base},
            {"timestamp": base + timedelta(minutes=30)},
            {"timestamp": base + timedelta(minutes=90)},
        ]
        counts = self._count_by_period(items, "timestamp", period_minutes=60)
        # First two in same hour, third in next
        assert len(counts) == 2


# --- Statistical aggregation patterns ---


class TestStatisticalAggregation:
    """Tests for statistical aggregation patterns."""

    def _variance(self, values):
        """Calculate variance."""
        if len(values) < 2:
            return 0
        mean = sum(values) / len(values)
        return sum((x - mean) ** 2 for x in values) / len(values)

    def _std_dev(self, values):
        """Calculate standard deviation."""
        return math.sqrt(self._variance(values))

    def _percentile(self, values, p):
        """Calculate percentile."""
        if not values:
            return 0
        sorted_values = sorted(values)
        k = (len(sorted_values) - 1) * (p / 100)
        f = math.floor(k)
        c = math.ceil(k)
        if f == c:
            return sorted_values[int(k)]
        return sorted_values[f] * (c - k) + sorted_values[c] * (k - f)

    def _median(self, values):
        """Calculate median."""
        return self._percentile(values, 50)

    def test_variance(self):
        values = [2, 4, 4, 4, 5, 5, 7, 9]
        variance = self._variance(values)
        assert abs(variance - 4) < 0.01

    def test_std_dev(self):
        values = [2, 4, 4, 4, 5, 5, 7, 9]
        std = self._std_dev(values)
        assert abs(std - 2) < 0.01

    def test_median_odd(self):
        values = [1, 3, 5, 7, 9]
        assert self._median(values) == 5

    def test_median_even(self):
        values = [1, 2, 3, 4]
        assert self._median(values) == 2.5

    def test_percentile_90th(self):
        values = list(range(1, 101))
        p90 = self._percentile(values, 90)
        assert abs(p90 - 90.1) < 0.5


# --- Running aggregation patterns ---


class TestRunningAggregation:
    """Tests for running/cumulative aggregation patterns."""

    def _running_sum(self, values):
        """Calculate running sum."""
        result = []
        total = 0
        for v in values:
            total += v
            result.append(total)
        return result

    def _running_average(self, values):
        """Calculate running average."""
        result = []
        total = 0
        for i, v in enumerate(values):
            total += v
            result.append(total / (i + 1))
        return result

    def _exponential_moving_average(self, values, alpha=0.3):
        """Calculate exponential moving average."""
        if not values:
            return []
        result = [values[0]]
        for v in values[1:]:
            ema = alpha * v + (1 - alpha) * result[-1]
            result.append(ema)
        return result

    def test_running_sum(self):
        values = [1, 2, 3, 4, 5]
        result = self._running_sum(values)
        assert result == [1, 3, 6, 10, 15]

    def test_running_average(self):
        values = [10, 20, 30]
        result = self._running_average(values)
        assert result[0] == 10
        assert result[1] == 15
        assert result[2] == 20

    def test_exponential_moving_average(self):
        values = [10, 10, 10, 20, 20, 20]
        result = self._exponential_moving_average(values, alpha=0.5)
        # Values should gradually approach 20
        assert result[0] == 10
        assert result[-1] > 15


# --- Distinct/unique aggregation patterns ---


class TestDistinctAggregation:
    """Tests for distinct value aggregation patterns."""

    def _count_distinct(self, items, field):
        """Count distinct values of a field."""
        return len(
            set(
                item.get(field) for item in items if item.get(field) is not None
            )
        )

    def _get_distinct_values(self, items, field):
        """Get distinct values of a field."""
        return list(
            set(
                item.get(field) for item in items if item.get(field) is not None
            )
        )

    def _count_distinct_combinations(self, items, fields):
        """Count distinct combinations of fields."""
        combos = set()
        for item in items:
            combo = tuple(item.get(f) for f in fields)
            if None not in combo:
                combos.add(combo)
        return len(combos)

    def test_count_distinct(self):
        items = [{"user_id": 1}, {"user_id": 2}, {"user_id": 1}]
        assert self._count_distinct(items, "user_id") == 2

    def test_get_distinct_values(self):
        items = [{"status": "a"}, {"status": "b"}, {"status": "a"}]
        distinct = self._get_distinct_values(items, "status")
        assert set(distinct) == {"a", "b"}

    def test_count_distinct_combinations(self):
        items = [
            {"a": 1, "b": "x"},
            {"a": 1, "b": "y"},
            {"a": 1, "b": "x"},  # duplicate
        ]
        assert self._count_distinct_combinations(items, ["a", "b"]) == 2


# --- Bucket aggregation patterns ---


class TestBucketAggregation:
    """Tests for bucket/histogram aggregation patterns."""

    def _bucket_by_range(self, items, field, ranges):
        """Bucket items by value ranges."""
        buckets = {r: [] for r in ranges}
        for item in items:
            value = item.get(field)
            if value is None:
                continue
            for r in ranges:
                if r[0] <= value < r[1]:
                    buckets[r].append(item)
                    break
        return buckets

    def _histogram(self, values, num_buckets=10):
        """Create histogram buckets."""
        if not values:
            return {}
        min_val = min(values)
        max_val = max(values)
        if min_val == max_val:
            return {(min_val, max_val + 1): len(values)}
        bucket_size = (max_val - min_val) / num_buckets
        buckets = defaultdict(int)
        for v in values:
            bucket_idx = int((v - min_val) / bucket_size)
            bucket_idx = min(bucket_idx, num_buckets - 1)
            bucket_start = min_val + bucket_idx * bucket_size
            bucket_end = bucket_start + bucket_size
            buckets[(bucket_start, bucket_end)] += 1
        return dict(buckets)

    def test_bucket_by_range(self):
        items = [{"score": 25}, {"score": 55}, {"score": 85}, {"score": 95}]
        ranges = [(0, 50), (50, 75), (75, 100)]
        buckets = self._bucket_by_range(items, "score", ranges)
        assert len(buckets[(0, 50)]) == 1
        assert len(buckets[(50, 75)]) == 1
        assert len(buckets[(75, 100)]) == 2

    def test_histogram(self):
        values = list(range(100))
        buckets = self._histogram(values, num_buckets=10)
        # Each bucket should have ~10 values
        assert len(buckets) == 10


# --- Top-N aggregation patterns ---


class TestTopNAggregation:
    """Tests for top-N aggregation patterns."""

    def _top_n(self, items, field, n, ascending=False):
        """Get top N items by field."""
        sorted_items = sorted(
            items,
            key=lambda x: x.get(
                field, float("-inf") if not ascending else float("inf")
            ),
            reverse=not ascending,
        )
        return sorted_items[:n]

    def _top_n_per_group(self, items, group_field, value_field, n):
        """Get top N items per group."""
        groups = defaultdict(list)
        for item in items:
            groups[item.get(group_field)].append(item)
        result = {}
        for group_key, group_items in groups.items():
            sorted_items = sorted(
                group_items,
                key=lambda x: x.get(value_field, 0),
                reverse=True,
            )
            result[group_key] = sorted_items[:n]
        return result

    def test_top_n(self):
        items = [
            {"name": "a", "score": 80},
            {"name": "b", "score": 95},
            {"name": "c", "score": 70},
        ]
        top = self._top_n(items, "score", 2)
        assert top[0]["name"] == "b"
        assert len(top) == 2

    def test_bottom_n(self):
        items = [
            {"name": "a", "score": 80},
            {"name": "b", "score": 95},
            {"name": "c", "score": 70},
        ]
        bottom = self._top_n(items, "score", 2, ascending=True)
        assert bottom[0]["name"] == "c"

    def test_top_n_per_group(self):
        items = [
            {"group": "A", "score": 80},
            {"group": "A", "score": 90},
            {"group": "B", "score": 85},
            {"group": "B", "score": 75},
        ]
        result = self._top_n_per_group(items, "group", "score", 1)
        assert result["A"][0]["score"] == 90
        assert result["B"][0]["score"] == 85


# --- Conditional aggregation patterns ---


class TestConditionalAggregation:
    """Tests for conditional aggregation patterns."""

    def _sum_if(self, items, field, predicate):
        """Sum field values where predicate is true."""
        return sum(item.get(field, 0) for item in items if predicate(item))

    def _avg_if(self, items, field, predicate):
        """Average field values where predicate is true."""
        matching = [
            item.get(field)
            for item in items
            if predicate(item) and item.get(field) is not None
        ]
        if not matching:
            return 0
        return sum(matching) / len(matching)

    def test_sum_if(self):
        items = [
            {"status": "active", "amount": 100},
            {"status": "inactive", "amount": 50},
            {"status": "active", "amount": 75},
        ]
        total = self._sum_if(
            items, "amount", lambda x: x.get("status") == "active"
        )
        assert total == 175

    def test_avg_if(self):
        items = [
            {"type": "A", "score": 80},
            {"type": "B", "score": 60},
            {"type": "A", "score": 100},
        ]
        avg = self._avg_if(items, "score", lambda x: x.get("type") == "A")
        assert avg == 90


# --- Pivot/cross-tab patterns ---


class TestPivotAggregation:
    """Tests for pivot/cross-tab aggregation patterns."""

    def _pivot_count(self, items, row_field, col_field):
        """Create pivot table with counts."""
        pivot = defaultdict(lambda: defaultdict(int))
        for item in items:
            row = item.get(row_field)
            col = item.get(col_field)
            pivot[row][col] += 1
        return {k: dict(v) for k, v in pivot.items()}

    def _pivot_sum(self, items, row_field, col_field, value_field):
        """Create pivot table with sums."""
        pivot = defaultdict(lambda: defaultdict(float))
        for item in items:
            row = item.get(row_field)
            col = item.get(col_field)
            pivot[row][col] += item.get(value_field, 0)
        return {k: dict(v) for k, v in pivot.items()}

    def test_pivot_count(self):
        items = [
            {"region": "East", "status": "active"},
            {"region": "East", "status": "inactive"},
            {"region": "West", "status": "active"},
            {"region": "East", "status": "active"},
        ]
        pivot = self._pivot_count(items, "region", "status")
        assert pivot["East"]["active"] == 2
        assert pivot["East"]["inactive"] == 1
        assert pivot["West"]["active"] == 1

    def test_pivot_sum(self):
        items = [
            {"region": "East", "category": "A", "sales": 100},
            {"region": "East", "category": "B", "sales": 200},
            {"region": "East", "category": "A", "sales": 50},
        ]
        pivot = self._pivot_sum(items, "region", "category", "sales")
        assert pivot["East"]["A"] == 150
        assert pivot["East"]["B"] == 200
