"""Tests for review pattern detectors."""

from __future__ import annotations

from bandito.review.aggregator import aggregate
from bandito.review.patterns import (
    detect_cost_concentration,
    detect_context_size_variance,
    detect_duplicate_tool_calls,
    detect_error_patterns,
    detect_latency_concentration,
    detect_model_diversity,
    detect_quality_by_model,
    detect_quality_cost_correlation,
    run_all,
)


class TestDetectDuplicateToolCalls:
    def test_no_duplicates(self, make_trace):
        traces = [make_trace()]
        stats = aggregate(traces)
        assert detect_duplicate_tool_calls(traces, stats, {}) is None

    def test_detects_duplicates(self, make_trace):
        trace = make_trace(extra_spans=[
            {"span_id": "t1", "name": "search", "kind": "tool",
             "input": {"q": "hello"}, "started_at": "2026-03-19T12:00:00Z",
             "ended_at": "2026-03-19T12:00:01Z", "status": "ok"},
            {"span_id": "t2", "name": "search", "kind": "tool",
             "input": {"q": "hello"}, "started_at": "2026-03-19T12:00:01Z",
             "ended_at": "2026-03-19T12:00:02Z", "status": "ok"},
        ])
        stats = aggregate([trace])
        result = detect_duplicate_tool_calls([trace], stats, {})
        assert result is not None
        assert result.name == "duplicate_tool_calls"
        assert result.severity == "warning"


class TestDetectCostConcentration:
    def test_no_concentration(self, make_trace):
        traces = [make_trace(cost=0.01) for _ in range(20)]
        stats = aggregate(traces)
        result = detect_cost_concentration(traces, stats, {})
        assert result is None  # Even distribution

    def test_detects_concentration(self, make_trace):
        traces = [make_trace(cost=0.001) for _ in range(18)]
        traces.extend(make_trace(cost=0.5) for _ in range(2))
        stats = aggregate(traces)
        result = detect_cost_concentration(traces, stats, {})
        assert result is not None
        assert result.name == "cost_concentration"

    def test_skips_small_sets(self, make_trace):
        traces = [make_trace(cost=1.0) for _ in range(5)]
        stats = aggregate(traces)
        result = detect_cost_concentration(traces, stats, {})
        assert result is None


class TestDetectLatencyConcentration:
    def test_detects_dominant_kind(self, make_trace):
        # Create traces where LLM spans dominate latency
        traces = [make_trace(latency=5.0)]
        stats = aggregate(traces)
        # With only LLM spans, they should be 100% of latency
        result = detect_latency_concentration(traces, stats, {})
        assert result is not None
        assert result.name == "latency_concentration"

    def test_no_concentration(self, make_trace):
        trace = make_trace(
            latency=1.0,
            extra_spans=[
                {"span_id": "t1", "name": "search", "kind": "tool",
                 "started_at": "2026-03-19T12:00:00Z",
                 "ended_at": "2026-03-19T12:00:05Z", "status": "ok"},
            ],
        )
        stats = aggregate([trace])
        # Check if any kind dominates — depends on relative latencies
        result = detect_latency_concentration([trace], stats, {})
        # With 1s LLM and 5s tool, tool should dominate
        if result:
            assert result.name == "latency_concentration"


class TestDetectErrorPatterns:
    def test_no_errors(self, make_trace):
        traces = [make_trace()]
        stats = aggregate(traces)
        assert detect_error_patterns(traces, stats, {}) is None

    def test_detects_errors(self, make_trace):
        traces = [make_trace(error="timeout") for _ in range(3)]
        stats = aggregate(traces)
        result = detect_error_patterns(traces, stats, {})
        assert result is not None
        assert result.name == "error_patterns"
        assert "3" in result.description

    def test_clusters_errors(self, make_trace):
        traces = [
            make_trace(error="timeout"),
            make_trace(error="timeout"),
            make_trace(error="rate limit"),
        ]
        stats = aggregate(traces)
        result = detect_error_patterns(traces, stats, {})
        assert result is not None
        # Should have 2 distinct patterns
        assert "2 distinct" in result.description


class TestDetectContextSizeVariance:
    def test_needs_enough_spans(self, make_trace):
        traces = [make_trace() for _ in range(5)]
        stats = aggregate(traces)
        assert detect_context_size_variance(traces, stats, {}) is None

    def test_detects_variance(self, make_trace):
        traces = []
        for i in range(12):
            tokens = 10 if i < 10 else 1000
            traces.append(make_trace(input_tokens=tokens))
        stats = aggregate(traces)
        result = detect_context_size_variance(traces, stats, {})
        # 1000 vs 10 = 100x variance
        assert result is not None
        assert result.name == "context_size_variance"


class TestDetectModelDiversity:
    def test_single_model(self, make_trace):
        traces = [make_trace(model="gpt-4o") for _ in range(5)]
        stats = aggregate(traces)
        result = detect_model_diversity(traces, stats, {})
        assert result is not None
        assert result.name == "model_diversity"
        assert "gpt-4o" in result.description

    def test_multiple_models(self, make_trace):
        traces = [
            make_trace(model="gpt-4o"),
            make_trace(model="claude-3-opus"),
        ]
        stats = aggregate(traces)
        result = detect_model_diversity(traces, stats, {})
        assert result is None


class TestDetectQualityCostCorrelation:
    def test_no_evaluations(self, make_trace):
        traces = [make_trace() for _ in range(20)]
        stats = aggregate(traces)
        assert detect_quality_cost_correlation(traces, stats, {}) is None

    def test_detects_cheap_equals_expensive(self, make_trace, make_evaluation):
        traces = []
        evals = {}
        for i in range(20):
            t = make_trace(trace_id=f"t-{i}", cost=0.001 * (i + 1))
            traces.append(t)
            evals[f"t-{i}"] = make_evaluation(f"t-{i}", human_score=1)
        stats = aggregate(traces, evals)
        result = detect_quality_cost_correlation(traces, stats, evals)
        # All score=1, so cheap should equal expensive
        assert result is not None
        assert result.name == "quality_cost_correlation"


class TestDetectQualityByModel:
    def test_no_evaluations(self, make_trace):
        traces = [make_trace() for _ in range(5)]
        stats = aggregate(traces)
        assert detect_quality_by_model(traces, stats, {}) is None

    def test_needs_multiple_models(self, make_trace, make_evaluation):
        traces = [make_trace(trace_id="t1")]
        evals = {"t1": make_evaluation("t1", human_score=1)}
        stats = aggregate(traces, evals)
        result = detect_quality_by_model(traces, stats, evals)
        assert result is None  # Only one model

    def test_reports_breakdown(self, make_trace, make_evaluation):
        traces = [
            make_trace(trace_id="t1", model="gpt-4o"),
            make_trace(trace_id="t2", model="claude-3-opus", provider="anthropic"),
        ]
        evals = {
            "t1": make_evaluation("t1", human_score=1),
            "t2": make_evaluation("t2", human_score=0),
        }
        stats = aggregate(traces, evals)
        result = detect_quality_by_model(traces, stats, evals)
        assert result is not None
        assert result.name == "quality_by_model"
        assert len(result.evidence) == 2


class TestRunAll:
    def test_returns_all_matching_patterns(self, make_trace):
        traces = [make_trace(model="gpt-4o", error="timeout") for _ in range(3)]
        stats = aggregate(traces)
        patterns = run_all(traces, stats)
        names = {p.name for p in patterns}
        assert "error_patterns" in names
        assert "model_diversity" in names
