"""
Extended Tests for Research Metrics

Phase 17: Token Counter & Metrics - Research Metrics Tests
Tests research metrics tracking, aggregation, and analysis.
"""

from datetime import datetime, timedelta, UTC
from unittest.mock import patch, MagicMock


class TestResearchMetrics:
    """Tests for research metrics tracking"""

    def test_research_duration_tracking(self):
        """Test research duration is tracked"""
        from local_deep_research.database.models import TokenUsage

        # Create mock usage record
        usage = MagicMock(spec=TokenUsage)
        usage.research_id = "test-123"
        usage.response_time_ms = 5000
        usage.created_at = datetime.now(UTC)

        assert usage.response_time_ms == 5000

    def test_source_count_tracking(self):
        """Test source count is tracked in context"""
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )

        context = {"sources_count": 15, "phase": "synthesis"}
        callback = TokenCountingCallback(research_context=context)

        assert callback.research_context["sources_count"] == 15

    def test_iteration_count_tracking(self):
        """Test iteration count is tracked"""
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )

        context = {"iteration": 3, "max_iterations": 10}
        callback = TokenCountingCallback(research_context=context)

        assert callback.research_context["iteration"] == 3

    def test_error_count_tracking(self):
        """Test error count is tracked via status"""
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )

        callback = TokenCountingCallback()
        callback.on_llm_start({"_type": "ChatOpenAI"}, ["test"])

        # Simulate error
        callback.on_llm_error(Exception("Test error"))

        assert callback.success_status == "error"

    def test_cache_hit_rate(self):
        """Test cache-related metrics can be tracked"""
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )

        context = {
            "cache_hits": 5,
            "cache_misses": 2,
        }
        callback = TokenCountingCallback(research_context=context)

        # Verify callback has the context
        assert callback.research_context == context

        cache_hit_rate = context["cache_hits"] / (
            context["cache_hits"] + context["cache_misses"]
        )
        assert cache_hit_rate > 0.7

    def test_llm_latency_metrics(self):
        """Test LLM latency is tracked"""
        # audit: PUNCHLIST reviewed 2026-05 — KEEP (TIMING_SLEEP).
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )
        import time

        callback = TokenCountingCallback()
        callback.on_llm_start({"_type": "ChatOpenAI"}, ["test"])

        time.sleep(0.01)  # 10ms

        mock_response = MagicMock()
        mock_response.llm_output = {}
        mock_response.generations = []

        callback.on_llm_end(mock_response)

        assert callback.response_time_ms >= 10

    def test_memory_usage_tracking(self):
        """Test memory metrics can be added to context"""
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )

        context = {
            "memory_usage_mb": 256,
        }
        callback = TokenCountingCallback(research_context=context)

        assert callback.research_context["memory_usage_mb"] == 256

    def test_socket_emit_metrics(self):
        """Test socket emission tracking"""
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )

        context = {
            "socket_emits": 10,
        }
        callback = TokenCountingCallback(research_context=context)

        assert callback.research_context["socket_emits"] == 10

    def test_pdf_processing_metrics(self):
        """Test PDF processing tracking"""
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )

        context = {
            "pdfs_processed": 5,
            "pdf_extraction_time_ms": 1500,
        }
        callback = TokenCountingCallback(research_context=context)

        assert callback.research_context["pdfs_processed"] == 5

    def test_synthesis_quality_metrics(self):
        """Test synthesis quality tracking"""
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )

        context = {
            "synthesis_score": 0.85,
        }
        callback = TokenCountingCallback(research_context=context)

        assert callback.research_context["synthesis_score"] == 0.85

    def test_source_diversity_score(self):
        """Test source diversity tracking"""
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )

        context = {
            "source_diversity": 0.72,
            "unique_domains": 8,
        }
        callback = TokenCountingCallback(research_context=context)

        assert callback.research_context["source_diversity"] == 0.72

    def test_relevance_score_tracking(self):
        """Test relevance score tracking"""
        from local_deep_research.metrics.token_counter import (
            TokenCountingCallback,
        )

        context = {
            "avg_relevance_score": 0.91,
        }
        callback = TokenCountingCallback(research_context=context)

        assert callback.research_context["avg_relevance_score"] == 0.91

    def test_user_satisfaction_metrics(self):
        """Test user rating tracking"""
        from local_deep_research.database.models import TokenUsage

        # Verify model supports research tracking
        usage = MagicMock(spec=TokenUsage)
        usage.research_id = "test-123"

        assert usage.research_id is not None


class TestMetricsAggregation:
    """Tests for metrics aggregation functionality"""

    @patch("local_deep_research.metrics.token_counter.TokenCounter")
    def test_aggregate_by_time_period(self, mock_counter_cls):
        """Test aggregation by time period"""
        mock_counter = MagicMock()
        mock_counter.get_overall_metrics.return_value = {
            "total_tokens": 10000,
            "total_cost": 0.50,
        }

        result = mock_counter.get_overall_metrics(time_filter="7d")

        assert "total_tokens" in result

    @patch("local_deep_research.metrics.token_counter.TokenCounter")
    def test_aggregate_by_user(self, mock_counter_cls):
        """Test aggregation by user"""
        mock_counter = MagicMock()
        mock_counter.get_research_metrics.return_value = {
            "research_id": "test-123",
            "total_tokens": 5000,
        }

        result = mock_counter.get_research_metrics("test-123")

        assert "total_tokens" in result

    @patch("local_deep_research.metrics.token_counter.TokenCounter")
    def test_aggregate_by_query_type(self, mock_counter_cls):
        """Test aggregation by query type"""
        mock_counter = MagicMock()
        mock_counter.get_overall_metrics.return_value = {
            "by_mode": {
                "quick": {"tokens": 2000},
                "detailed": {"tokens": 8000},
            }
        }

        result = mock_counter.get_overall_metrics()

        # Should support mode filtering
        assert "by_mode" in result

    @patch("local_deep_research.metrics.token_counter.TokenCounter")
    def test_aggregate_by_model(self, mock_counter_cls):
        """Test aggregation by model"""
        mock_counter = MagicMock()
        mock_counter.get_overall_metrics.return_value = {
            "by_model": {
                "gpt-4": {"tokens": 5000},
                "gpt-3.5-turbo": {"tokens": 3000},
            }
        }

        result = mock_counter.get_overall_metrics()

        assert "by_model" in result

    def test_percentile_calculations(self):
        """Test percentile calculation capability"""
        import statistics

        response_times = [100, 150, 200, 250, 300, 350, 400, 450, 500, 550]

        # Calculate p50, p95, p99
        p50 = statistics.median(response_times)
        p95 = sorted(response_times)[int(len(response_times) * 0.95)]

        assert p50 == 325.0  # Median of 10 values
        assert p95 >= 500

    def test_moving_average_calculation(self):
        """Test moving average capability"""
        values = [100, 120, 110, 130, 125, 135, 140, 145, 150, 155]
        window_size = 3

        # Calculate simple moving average for last window
        sma = sum(values[-window_size:]) / window_size

        assert sma == 150.0

    def test_trend_detection(self):
        """Test trend detection capability"""
        # Increasing trend
        values = [100, 110, 120, 130, 140, 150]

        # Simple linear regression would show positive slope
        slope = (values[-1] - values[0]) / (len(values) - 1)

        assert slope > 0  # Positive trend

    def test_anomaly_detection(self):
        """Test anomaly detection capability"""
        import statistics

        values = [100, 105, 98, 102, 101, 500, 99, 103]  # 500 is anomaly

        mean = statistics.mean(values)
        stdev = statistics.stdev(values)

        # Values more than 2 stdev from mean are anomalies
        anomalies = [v for v in values if abs(v - mean) > 2 * stdev]

        assert 500 in anomalies

    def test_histogram_generation(self):
        """Test histogram bucket generation"""
        values = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

        # Create buckets with bucket_size = 20
        bucket_size = 20
        buckets = {}

        for v in values:
            bucket = (v // bucket_size) * bucket_size
            buckets[bucket] = buckets.get(bucket, 0) + 1

        # Values 10 -> bucket 0, 20-39 -> bucket 20, 40-59 -> bucket 40, etc.
        # So we get 6 buckets: 0, 20, 40, 60, 80, 100
        assert len(buckets) == 6

    def test_metrics_export_json(self):
        """Test metrics can be exported as JSON"""
        import json

        metrics = {
            "total_tokens": 10000,
            "by_model": {"gpt-4": 5000},
            "timestamp": datetime.now(UTC).isoformat(),
        }

        json_str = json.dumps(metrics)

        assert "total_tokens" in json_str
        assert "10000" in json_str

    def test_metrics_export_csv(self):
        """Test metrics can be exported as CSV format"""
        import csv
        from io import StringIO

        metrics = [
            {"date": "2024-01-01", "tokens": 1000},
            {"date": "2024-01-02", "tokens": 1500},
        ]

        output = StringIO()
        writer = csv.DictWriter(output, fieldnames=["date", "tokens"])
        writer.writeheader()
        writer.writerows(metrics)

        csv_str = output.getvalue()

        assert "date,tokens" in csv_str
        assert "1000" in csv_str

    def test_metrics_comparison(self):
        """Test period comparison"""
        current_period = {"tokens": 10000, "cost": 0.50}
        previous_period = {"tokens": 8000, "cost": 0.40}

        change_tokens = (
            (current_period["tokens"] - previous_period["tokens"])
            / previous_period["tokens"]
            * 100
        )
        change_cost = (
            (current_period["cost"] - previous_period["cost"])
            / previous_period["cost"]
            * 100
        )

        assert change_tokens == 25.0  # 25% increase
        # Use approximate comparison for floating point
        assert abs(change_cost - 25.0) < 0.001

    def test_baseline_comparison(self):
        """Test comparison against baseline"""
        baseline = {"avg_response_ms": 500, "error_rate": 0.01}
        current = {"avg_response_ms": 450, "error_rate": 0.008}

        # Check if current is better than baseline
        response_improved = (
            current["avg_response_ms"] < baseline["avg_response_ms"]
        )
        error_improved = current["error_rate"] < baseline["error_rate"]

        assert response_improved is True
        assert error_improved is True

    def test_metrics_cleanup_old_data(self):
        """Test old metrics cleanup capability"""
        cutoff = datetime.now(UTC) - timedelta(days=90)

        # Simulate records
        records = [
            {"created_at": datetime.now(UTC) - timedelta(days=100)},
            {"created_at": datetime.now(UTC) - timedelta(days=50)},
            {"created_at": datetime.now(UTC) - timedelta(days=10)},
        ]

        # Filter old records
        old_records = [r for r in records if r["created_at"] < cutoff]

        assert len(old_records) == 1

    def test_metrics_storage_optimization(self):
        """Test metrics can be stored efficiently"""
        # Aggregate daily instead of per-call
        daily_aggregate = {
            "date": "2024-01-15",
            "total_tokens": 50000,
            "total_cost": 2.50,
            "call_count": 100,
        }

        # Calculate averages
        avg_tokens = (
            daily_aggregate["total_tokens"] / daily_aggregate["call_count"]
        )
        avg_cost = daily_aggregate["total_cost"] / daily_aggregate["call_count"]

        assert avg_tokens == 500
        assert avg_cost == 0.025


class TestQueryUtils:
    """Tests for query utility functions"""

    def test_time_filter_7d(self):
        """Test 7-day time filter"""
        from local_deep_research.metrics.query_utils import (
            get_time_filter_condition,
        )
        from local_deep_research.database.models import TokenUsage

        condition = get_time_filter_condition("7d", TokenUsage.timestamp)

        assert condition is not None

    def test_time_filter_30d(self):
        """Test 30-day time filter"""
        from local_deep_research.metrics.query_utils import (
            get_time_filter_condition,
        )
        from local_deep_research.database.models import TokenUsage

        condition = get_time_filter_condition("30d", TokenUsage.timestamp)

        assert condition is not None

    def test_time_filter_all(self):
        """Test 'all' time filter returns None"""
        from local_deep_research.metrics.query_utils import (
            get_time_filter_condition,
        )
        from local_deep_research.database.models import TokenUsage

        condition = get_time_filter_condition("all", TokenUsage.timestamp)

        assert condition is None

    def test_mode_filter_quick(self):
        """Test quick mode filter"""
        from local_deep_research.metrics.query_utils import (
            get_research_mode_condition,
        )
        from local_deep_research.database.models import TokenUsage

        condition = get_research_mode_condition(
            "quick", TokenUsage.research_mode
        )

        assert condition is not None

    def test_mode_filter_detailed(self):
        """Test detailed mode filter"""
        from local_deep_research.metrics.query_utils import (
            get_research_mode_condition,
        )
        from local_deep_research.database.models import TokenUsage

        condition = get_research_mode_condition(
            "detailed", TokenUsage.research_mode
        )

        assert condition is not None


class TestSearchTracker:
    """Tests for search tracking functionality"""

    @patch("local_deep_research.metrics.search_tracker.SearchTracker")
    def test_get_search_metrics(self, mock_tracker):
        """Test getting search metrics"""
        mock_tracker.get_search_metrics.return_value = {
            "total_searches": 100,
            "by_engine": {"google": 50, "bing": 50},
        }

        result = mock_tracker.get_search_metrics()

        assert "total_searches" in result


class TestCostCalculator:
    """Tests for cost calculation"""

    def test_local_model_free(self):
        """Test local models are free"""
        from local_deep_research.metrics.pricing.pricing_fetcher import (
            PricingFetcher,
        )

        # Local models should return 0 cost
        fetcher = PricingFetcher()

        # Ollama models should be free - use static_pricing attribute
        pricing = fetcher.static_pricing.get("ollama", {}).get("llama2", {})

        # If no pricing info, default should be 0
        assert pricing.get("input_cost", 0) == 0
        assert pricing.get("output_cost", 0) == 0
