"""Unit tests for get_strategy_analytics and get_rate_limiting_analytics.

These test the aggregation logic directly (mocking DB), not through HTTP endpoints.
Production code: src/local_deep_research/web/routes/metrics_routes.py
"""

from unittest.mock import MagicMock, Mock, patch

import pytest

from local_deep_research.web.routes.metrics_routes import (
    get_rate_limiting_analytics,
    get_strategy_analytics,
)

_MOD = "local_deep_research.web.routes.metrics_routes"


def _mock_session_ctx(mock_ctx, session):
    """Wire up get_user_db_session mock as context manager."""
    mock_ctx.return_value.__enter__ = Mock(return_value=session)
    mock_ctx.return_value.__exit__ = Mock(return_value=False)


# ---------------------------------------------------------------------------
# get_strategy_analytics
# ---------------------------------------------------------------------------


class TestGetStrategyAnalytics:
    """Tests for get_strategy_analytics aggregation logic."""

    @patch(f"{_MOD}.flask_session", {})
    @patch(f"{_MOD}.get_user_db_session")
    def test_no_username_returns_error(self, mock_ctx):
        result = get_strategy_analytics(username=None)
        analytics = result["strategy_analytics"]
        assert analytics["error"] == "No user session"
        assert analytics["total_research"] == 0
        assert analytics["strategy_usage"] == []

    @patch(f"{_MOD}.get_user_db_session")
    def test_empty_strategy_table(self, mock_ctx):
        session = MagicMock()
        session.query.return_value.count.return_value = 0
        _mock_session_ctx(mock_ctx, session)

        result = get_strategy_analytics(username="alice")
        analytics = result["strategy_analytics"]
        assert "message" in analytics
        assert "not yet available" in analytics["message"]
        assert analytics["total_research"] == 0

    @patch(f"{_MOD}.get_user_db_session")
    def test_single_strategy(self, mock_ctx):
        session = MagicMock()
        # .count() for initial check
        session.query.return_value.count.return_value = 1

        # Chain for grouped query
        query_chain = session.query.return_value
        query_chain.filter.return_value = query_chain
        query_chain.group_by.return_value.order_by.return_value.all.return_value = [
            ("quick", 5),
        ]
        # total_query.count()
        query_chain.filter.return_value.count.return_value = 5
        _mock_session_ctx(mock_ctx, session)

        result = get_strategy_analytics(period="30d", username="alice")
        analytics = result["strategy_analytics"]
        assert analytics["most_popular_strategy"] == "quick"
        assert len(analytics["strategy_usage"]) == 1
        assert analytics["strategy_usage"][0]["percentage"] == 100.0
        assert analytics["strategy_usage"][0]["count"] == 5
        assert analytics["total_research"] == 5

    @patch(f"{_MOD}.get_user_db_session")
    def test_multiple_strategies_sorted_desc(self, mock_ctx):
        session = MagicMock()
        session.query.return_value.count.return_value = 3

        query_chain = session.query.return_value
        query_chain.filter.return_value = query_chain
        query_chain.group_by.return_value.order_by.return_value.all.return_value = [
            ("deep", 6),
            ("quick", 4),
        ]
        query_chain.filter.return_value.count.return_value = 10
        _mock_session_ctx(mock_ctx, session)

        result = get_strategy_analytics(period="30d", username="alice")
        analytics = result["strategy_analytics"]
        assert analytics["most_popular_strategy"] == "deep"
        assert len(analytics["strategy_usage"]) == 2
        # Percentages should sum to 100
        total_pct = sum(s["percentage"] for s in analytics["strategy_usage"])
        assert total_pct == pytest.approx(100.0, abs=0.2)

    @patch(f"{_MOD}.get_user_db_session")
    def test_period_all_no_time_filter(self, mock_ctx):
        """period='all' → days=None → no time filter."""
        session = MagicMock()
        # Initial count
        base_query = MagicMock()
        session.query.return_value = base_query
        base_query.count.return_value = 1
        # No filter should be called for "all"
        base_query.group_by.return_value.order_by.return_value.all.return_value = [
            ("quick", 1),
        ]
        _mock_session_ctx(mock_ctx, session)

        result = get_strategy_analytics(period="all", username="alice")
        analytics = result["strategy_analytics"]
        assert analytics["total_research"] == 1
        # filter should NOT be called (no time cutoff)
        base_query.filter.assert_not_called()

    @patch(f"{_MOD}.get_user_db_session")
    def test_period_7d_returns_valid_result(self, mock_ctx):
        session = MagicMock()
        session.query.return_value.count.return_value = 1
        query_chain = session.query.return_value
        query_chain.filter.return_value = query_chain
        query_chain.group_by.return_value.order_by.return_value.all.return_value = []
        query_chain.filter.return_value.count.return_value = 0
        _mock_session_ctx(mock_ctx, session)

        result = get_strategy_analytics(period="7d", username="alice")
        assert "strategy_analytics" in result
        assert result["strategy_analytics"]["strategy_usage"] == []

    @patch(f"{_MOD}.get_user_db_session")
    def test_unknown_period_defaults_to_30(self, mock_ctx):
        session = MagicMock()
        session.query.return_value.count.return_value = 1
        query_chain = session.query.return_value
        query_chain.filter.return_value = query_chain
        query_chain.group_by.return_value.order_by.return_value.all.return_value = []
        query_chain.filter.return_value.count.return_value = 0
        _mock_session_ctx(mock_ctx, session)

        # Should not raise, defaults to 30 days
        result = get_strategy_analytics(period="999d", username="alice")
        assert "strategy_analytics" in result
        # No error key means it ran successfully with default period
        assert "error" not in result["strategy_analytics"]

    @patch(f"{_MOD}.get_user_db_session")
    def test_exception_returns_fallback(self, mock_ctx):
        mock_ctx.return_value.__enter__ = Mock(
            side_effect=RuntimeError("DB down")
        )
        mock_ctx.return_value.__exit__ = Mock(return_value=False)

        result = get_strategy_analytics(username="alice")
        analytics = result["strategy_analytics"]
        assert analytics["error"] == "Failed to retrieve strategy data"
        assert analytics["total_research"] == 0

    @patch(f"{_MOD}.get_user_db_session")
    def test_strategy_distribution_matches_usage(self, mock_ctx):
        session = MagicMock()
        session.query.return_value.count.return_value = 2
        query_chain = session.query.return_value
        query_chain.filter.return_value = query_chain
        query_chain.group_by.return_value.order_by.return_value.all.return_value = [
            ("deep", 3),
            ("quick", 7),
        ]
        query_chain.filter.return_value.count.return_value = 10
        _mock_session_ctx(mock_ctx, session)

        result = get_strategy_analytics(period="30d", username="alice")
        analytics = result["strategy_analytics"]
        assert analytics["strategy_distribution"] == {"deep": 3, "quick": 7}

    @patch(f"{_MOD}.get_user_db_session")
    def test_zero_total_research_no_division_error(self, mock_ctx):
        """When total_research is 0 after time filter, percentages should be 0."""
        session = MagicMock()
        session.query.return_value.count.return_value = 1  # has records overall
        query_chain = session.query.return_value
        query_chain.filter.return_value = query_chain
        # But after time filter, nothing found - yet group_by returns empty
        query_chain.group_by.return_value.order_by.return_value.all.return_value = []
        query_chain.filter.return_value.count.return_value = 0
        _mock_session_ctx(mock_ctx, session)

        result = get_strategy_analytics(period="7d", username="alice")
        analytics = result["strategy_analytics"]
        assert analytics["total_research"] == 0
        assert analytics["strategy_usage"] == []

    @patch(f"{_MOD}.get_user_db_session")
    def test_total_research_with_strategy_sums_counts(self, mock_ctx):
        session = MagicMock()
        session.query.return_value.count.return_value = 5
        query_chain = session.query.return_value
        query_chain.filter.return_value = query_chain
        query_chain.group_by.return_value.order_by.return_value.all.return_value = [
            ("deep", 3),
            ("quick", 2),
        ]
        query_chain.filter.return_value.count.return_value = 5
        _mock_session_ctx(mock_ctx, session)

        result = get_strategy_analytics(period="30d", username="alice")
        analytics = result["strategy_analytics"]
        assert analytics["total_research_with_strategy"] == 5


# ---------------------------------------------------------------------------
# get_rate_limiting_analytics
# ---------------------------------------------------------------------------


def _make_attempt(
    engine_type, success, wait_time, error_type=None, timestamp=100.0
):
    """Create a mock RateLimitAttempt."""
    a = Mock()
    a.engine_type = engine_type
    a.success = success
    a.wait_time = wait_time
    a.error_type = error_type
    a.timestamp = timestamp
    return a


def _make_estimate(
    engine_type,
    success_rate,
    base_wait=1.0,
    min_wait=0.5,
    max_wait=5.0,
    total_attempts=10,
    last_updated=1000.0,
):
    """Create a mock RateLimitEstimate."""
    e = Mock()
    e.engine_type = engine_type
    e.success_rate = success_rate
    e.base_wait_seconds = base_wait
    e.min_wait_seconds = min_wait
    e.max_wait_seconds = max_wait
    e.total_attempts = total_attempts
    e.last_updated = last_updated
    return e


class TestGetRateLimitingAnalytics:
    """Tests for get_rate_limiting_analytics aggregation logic."""

    @patch(f"{_MOD}.flask_session", {})
    @patch(f"{_MOD}.get_user_db_session")
    def test_no_username_returns_error(self, mock_ctx):
        result = get_rate_limiting_analytics(username=None)
        rl = result["rate_limiting"]
        assert rl["error"] == "No user session"
        assert rl["total_attempts"] == 0
        assert rl["engine_stats"] == []

    @patch(f"{_MOD}.get_user_db_session")
    def test_no_attempts_all_zeros(self, mock_ctx):
        session = MagicMock()
        query = session.query.return_value
        query.filter.return_value = query
        query.count.return_value = 0
        query.all.return_value = []
        query.scalar.return_value = 0
        query.distinct.return_value = query
        _mock_session_ctx(mock_ctx, session)

        result = get_rate_limiting_analytics(period="30d", username="alice")
        rl = result["rate_limiting"]
        assert rl["total_attempts"] == 0
        assert rl["successful_attempts"] == 0
        assert rl["success_rate"] == 0
        assert rl["avg_wait_time"] == 0
        assert rl["engine_stats"] == []

    @patch(f"{_MOD}.get_user_db_session")
    @patch("time.time", return_value=1_000_000.0)
    def test_mixed_success_failure(self, mock_time, mock_ctx):
        session = MagicMock()
        attempts = [
            _make_attempt("google", True, 0.5),
            _make_attempt("google", True, 0.3),
            _make_attempt("google", False, 1.0, error_type="RateLimitError"),
        ]

        query = session.query.return_value
        query.filter.return_value = query
        query.distinct.return_value = query
        query.count.side_effect = iter([3, 2, 1]).__next__
        query.scalar.return_value = 1
        # .all() call order: (1) attempts, (2) engine_types, (3) estimates
        query.all.side_effect = [
            attempts,
            [Mock(engine_type="google")],
            [],  # no estimates
        ]
        _mock_session_ctx(mock_ctx, session)

        result = get_rate_limiting_analytics(period="30d", username="alice")
        rl = result["rate_limiting"]
        assert rl["total_attempts"] == 3
        assert rl["successful_attempts"] == 2
        assert rl["failed_attempts"] == 1
        assert rl["success_rate"] == pytest.approx(66.67, abs=0.1)
        assert rl["avg_wait_time"] == pytest.approx(0.6, abs=0.01)

    @patch(f"{_MOD}.get_user_db_session")
    @patch("time.time", return_value=1_000_000.0)
    def test_rate_limit_events_count(self, mock_time, mock_ctx):
        """Failures with error_type='RateLimitError' are counted as rate_limit_events."""

        session = MagicMock()
        attempts = [
            _make_attempt("bing", False, 2.0, error_type="RateLimitError"),
            _make_attempt("bing", False, 1.0, error_type="TimeoutError"),
            _make_attempt("bing", True, 0.5),
        ]

        query = session.query.return_value
        query.filter.return_value = query
        query.distinct.return_value = query
        query.count.side_effect = iter([3, 1, 1]).__next__
        query.scalar.return_value = 1
        query.all.side_effect = [
            attempts,
            [Mock(engine_type="bing")],
            [],
        ]
        _mock_session_ctx(mock_ctx, session)

        result = get_rate_limiting_analytics(period="30d", username="alice")
        assert result["rate_limiting"]["rate_limit_events"] == 1

    @patch(f"{_MOD}.get_user_db_session")
    @patch("time.time", return_value=1_000_000.0)
    def test_avg_successful_wait(self, mock_time, mock_ctx):
        session = MagicMock()
        attempts = [
            _make_attempt("google", True, 0.4),
            _make_attempt("google", True, 0.6),
            _make_attempt("google", False, 2.0),
        ]

        query = session.query.return_value
        query.filter.return_value = query
        query.distinct.return_value = query
        query.count.side_effect = iter([3, 2, 0]).__next__
        query.scalar.return_value = 1
        query.all.side_effect = [
            attempts,
            [Mock(engine_type="google")],
            [],
        ]
        _mock_session_ctx(mock_ctx, session)

        result = get_rate_limiting_analytics(period="30d", username="alice")
        rl = result["rate_limiting"]
        assert rl["avg_successful_wait"] == pytest.approx(0.5, abs=0.01)
        assert rl["avg_wait_time"] == pytest.approx(1.0, abs=0.01)

    @patch(f"{_MOD}.get_user_db_session")
    @patch("time.time", return_value=1_000_000.0)
    def test_engine_status_healthy_with_estimate(self, mock_time, mock_ctx):
        """estimate.success_rate > 0.8 → 'healthy'."""

        session = MagicMock()
        attempts = [_make_attempt("google", True, 0.5)]

        query = session.query.return_value
        query.filter.return_value = query
        query.distinct.return_value = query
        query.count.side_effect = iter([1, 1, 0]).__next__
        query.scalar.return_value = 1
        # .all() order: (1) attempts, (2) engine_types, (3) estimates
        query.all.side_effect = [
            attempts,
            [Mock(engine_type="google")],
            [_make_estimate("google", 0.9)],
        ]
        _mock_session_ctx(mock_ctx, session)

        result = get_rate_limiting_analytics(period="30d", username="alice")
        engine_stats = result["rate_limiting"]["engine_stats"]
        assert len(engine_stats) == 1
        assert engine_stats[0]["status"] == "healthy"
        assert result["rate_limiting"]["healthy_engines"] == 1

    @patch(f"{_MOD}.get_user_db_session")
    @patch("time.time", return_value=1_000_000.0)
    def test_engine_status_degraded_with_estimate(self, mock_time, mock_ctx):
        """0.5 < estimate.success_rate <= 0.8 → 'degraded'."""

        session = MagicMock()
        attempts = [_make_attempt("google", True, 0.5)]

        query = session.query.return_value
        query.filter.return_value = query
        query.distinct.return_value = query
        query.count.side_effect = iter([1, 1, 0]).__next__
        query.scalar.return_value = 1
        query.all.side_effect = [
            attempts,
            [Mock(engine_type="google")],
            [_make_estimate("google", 0.7)],
        ]
        _mock_session_ctx(mock_ctx, session)

        result = get_rate_limiting_analytics(period="30d", username="alice")
        assert (
            result["rate_limiting"]["engine_stats"][0]["status"] == "degraded"
        )
        assert result["rate_limiting"]["degraded_engines"] == 1

    @patch(f"{_MOD}.get_user_db_session")
    @patch("time.time", return_value=1_000_000.0)
    def test_engine_status_poor_with_estimate(self, mock_time, mock_ctx):
        """estimate.success_rate <= 0.5 → 'poor'."""

        session = MagicMock()
        attempts = [_make_attempt("google", False, 2.0)]

        query = session.query.return_value
        query.filter.return_value = query
        query.distinct.return_value = query
        query.count.side_effect = iter([1, 0, 1]).__next__
        query.scalar.return_value = 1
        query.all.side_effect = [
            attempts,
            [Mock(engine_type="google")],
            [_make_estimate("google", 0.3)],
        ]
        _mock_session_ctx(mock_ctx, session)

        result = get_rate_limiting_analytics(period="30d", username="alice")
        assert result["rate_limiting"]["engine_stats"][0]["status"] == "poor"
        assert result["rate_limiting"]["poor_engines"] == 1

    @patch(f"{_MOD}.get_user_db_session")
    @patch("time.time", return_value=1_000_000.0)
    def test_no_estimate_falls_back_to_recent_rate(self, mock_time, mock_ctx):
        """No estimate → uses recent_success_rate with 80/50 thresholds."""

        session = MagicMock()
        # 1 out of 2 = 50% → poor (not > 50)
        attempts = [
            _make_attempt("bing", True, 0.5),
            _make_attempt("bing", False, 1.0),
        ]

        query = session.query.return_value
        query.filter.return_value = query
        query.distinct.return_value = query
        query.count.side_effect = iter([2, 1, 0]).__next__
        query.scalar.return_value = 1
        query.all.side_effect = [
            attempts,
            [Mock(engine_type="bing")],
            [],  # no estimates
        ]
        _mock_session_ctx(mock_ctx, session)

        result = get_rate_limiting_analytics(period="30d", username="alice")
        engine_stat = result["rate_limiting"]["engine_stats"][0]
        assert engine_stat["status"] == "poor"
        assert engine_stat["recent_success_rate"] == 50.0

    @patch(f"{_MOD}.get_user_db_session")
    @patch("time.time", return_value=1_000_000.0)
    def test_multiple_engines_aggregated(self, mock_time, mock_ctx):
        session = MagicMock()
        attempts = [
            _make_attempt("google", True, 0.3),
            _make_attempt("bing", True, 0.5),
            _make_attempt("bing", False, 1.0),
        ]

        query = session.query.return_value
        query.filter.return_value = query
        query.distinct.return_value = query
        query.count.side_effect = iter([3, 2, 0]).__next__
        query.scalar.return_value = 2
        query.all.side_effect = [
            attempts,
            [Mock(engine_type="google"), Mock(engine_type="bing")],
            [],  # no estimates
        ]
        _mock_session_ctx(mock_ctx, session)

        result = get_rate_limiting_analytics(period="30d", username="alice")
        rl = result["rate_limiting"]
        assert rl["total_engines_tracked"] == 2
        engines = {s["engine"]: s for s in rl["engine_stats"]}
        assert engines["google"]["recent_success_rate"] == 100.0
        assert engines["bing"]["recent_success_rate"] == 50.0

    @patch(f"{_MOD}.get_user_db_session")
    @patch("time.time", return_value=1_000_000.0)
    def test_period_all_cutoff_zero(self, mock_time, mock_ctx):
        """period='all' → cutoff_time=0 → no time filter applied."""

        session = MagicMock()
        query = session.query.return_value
        query.filter.return_value = query
        query.distinct.return_value = query
        query.count.return_value = 0
        query.all.return_value = []
        query.scalar.return_value = 0
        _mock_session_ctx(mock_ctx, session)

        result = get_rate_limiting_analytics(period="all", username="alice")
        assert "rate_limiting" in result

    @patch(f"{_MOD}.get_user_db_session")
    def test_exception_returns_fallback(self, mock_ctx):
        mock_ctx.return_value.__enter__ = Mock(
            side_effect=RuntimeError("DB down")
        )
        mock_ctx.return_value.__exit__ = Mock(return_value=False)

        result = get_rate_limiting_analytics(username="alice")
        rl = result["rate_limiting"]
        assert "error" in rl
        assert rl["total_attempts"] == 0
        assert rl["engine_stats"] == []
