"""
Tests for _get_metrics_from_encrypted_db() with non-empty data.

Existing tests mock all DB queries to return empty results, leaving the
data-processing branches (provider map, recent researches, rate limit stats,
engine stats, alternative time filters) completely uncovered.
"""

import time
from typing import Any
from unittest.mock import MagicMock, Mock, patch

from local_deep_research.metrics.token_counter import TokenCounter


def _patch_get_user_db_session(mock_session: MagicMock) -> Any:
    ctx = MagicMock()
    ctx.__enter__ = Mock(return_value=mock_session)
    ctx.__exit__ = Mock(return_value=False)
    return patch(
        "local_deep_research.database.session_context.get_user_db_session",
        return_value=ctx,
    )


def _make_session(
    all_sequence: list[Any] | None = None,
    count_sequence: list[int] | None = None,
) -> MagicMock:
    """Build a mock session using a single shared query chain (like existing tests).

    Args:
        all_sequence: list of return values for successive .all() calls.
            With "30d" period and empty data, .all() is called 5 times:
              1. sample_researches
              2. model_stats
              3. recent_research_data
              4. rate_limit_attempts
              5. engine_types
            With non-empty model_stats, an extra call is inserted after #2 (providers).
            With non-empty recent_data, an extra call is inserted after #3 (query_results).
            With non-empty engine_types, an extra call is appended (estimates).
        count_sequence: list of return values for successive .count() calls.
            Default: [0, 0, 0] (total_attempts, successful, rate_limit_events)
    """
    mock_session = MagicMock()
    q = MagicMock()
    mock_session.query.return_value = q
    for attr in (
        "filter",
        "filter_by",
        "with_entities",
        "group_by",
        "order_by",
        "limit",
        "distinct",
    ):
        getattr(q, attr).return_value = q

    q.scalar.return_value = 0
    q.first.return_value = MagicMock(
        total_input_tokens=500,
        total_output_tokens=500,
        avg_input_tokens=50,
        avg_output_tokens=50,
        avg_total_tokens=100,
    )

    q.all.return_value = []
    if all_sequence is not None:
        all_iter = iter(all_sequence)
        q.all.side_effect = lambda: next(all_iter, [])

    if count_sequence is not None:
        count_iter = iter(count_sequence)
        q.count.side_effect = lambda: next(count_iter, 0)
    else:
        q.count.return_value = 0

    return mock_session


class TestMetricsWithModelData:
    """Tests for model data processing branches when data is present."""

    def test_provider_map_batch_loaded(self):
        """Provider info is batch-loaded and by_model list is constructed."""
        counter = TokenCounter()

        model_stat = MagicMock(
            model_name="gpt-4",
            tokens=500,
            calls=10,
            prompt_tokens=300,
            completion_tokens=200,
        )
        # provider_results rows are unpacked as tuples: (model_name, model_provider)
        provider_row = ("gpt-4", "openai")

        # .all() order: samples, model_stats, providers(added), recent, attempts, engines
        mock_session = _make_session(
            all_sequence=[
                [],  # 1. sample_researches
                [model_stat],  # 2. model_stats → triggers provider batch load
                [provider_row],  # 2b. provider_results (tuple rows)
                [],  # 3. recent_research_data
                [],  # 4. rate_limit_attempts
                [],  # 5. engine_types
            ]
        )

        with patch("flask.session", {"username": "testuser"}):
            with _patch_get_user_db_session(mock_session):
                result = counter._get_metrics_from_encrypted_db("30d", "all")

        assert len(result["by_model"]) == 1
        assert result["by_model"][0]["model"] == "gpt-4"
        assert result["by_model"][0]["provider"] == "openai"

    def test_by_model_unknown_provider_fallback(self):
        """Models without provider info get 'unknown' as default."""
        counter = TokenCounter()

        model_stat = MagicMock(
            model_name="llama-3",
            tokens=200,
            calls=5,
            prompt_tokens=100,
            completion_tokens=100,
        )

        mock_session = _make_session(
            all_sequence=[
                [],  # 1. sample_researches
                [model_stat],  # 2. model_stats
                [],  # 2b. provider_results (empty)
                [],  # 3. recent_research_data
                [],  # 4. rate_limit_attempts
                [],  # 5. engine_types
            ]
        )

        with patch("flask.session", {"username": "testuser"}):
            with _patch_get_user_db_session(mock_session):
                result = counter._get_metrics_from_encrypted_db("30d", "all")

        assert result["by_model"][0]["provider"] == "unknown"

    def test_recent_research_query_map_loaded(self):
        """Recent research queries are batch-loaded and mapped."""
        counter = TokenCounter()

        recent = MagicMock(
            research_id="res-123",
            token_count=750,
            latest_timestamp="2026-01-01T00:00:00",
        )
        # query_results rows are unpacked as tuples: (research_id, research_query)
        query_row = ("res-123", "climate change effects")

        mock_session = _make_session(
            all_sequence=[
                [],  # 1. sample_researches
                [],  # 2. model_stats
                [recent],  # 3. recent_research_data → triggers query batch load
                [query_row],  # 3b. query_results (tuple rows)
                [],  # 4. rate_limit_attempts
                [],  # 5. engine_types
            ]
        )

        with patch("flask.session", {"username": "testuser"}):
            with _patch_get_user_db_session(mock_session):
                result = counter._get_metrics_from_encrypted_db("30d", "all")

        assert len(result["recent_researches"]) == 1
        assert (
            result["recent_researches"][0]["query"] == "climate change effects"
        )
        assert result["recent_researches"][0]["tokens"] == 750

    def test_recent_research_fallback_query_text(self):
        """When no query text found, falls back to 'Research {id}' format."""
        counter = TokenCounter()

        recent = MagicMock(
            research_id="res-456",
            token_count=300,
            latest_timestamp="2026-02-01",
        )

        mock_session = _make_session(
            all_sequence=[
                [],  # 1. sample_researches
                [],  # 2. model_stats
                [recent],  # 3. recent_research_data
                [],  # 3b. query_results (empty)
                [],  # 4. rate_limit_attempts
                [],  # 5. engine_types
            ]
        )

        with patch("flask.session", {"username": "testuser"}):
            with _patch_get_user_db_session(mock_session):
                result = counter._get_metrics_from_encrypted_db("30d", "all")

        assert result["recent_researches"][0]["query"] == "Research res-456"


class TestMetricsTimeFilters:
    """Tests for alternative time filter periods.

    Each test asserts ``"rate_limiting" in result``. That key is produced only
    on the full success path of ``_get_metrics_from_encrypted_db``; it is absent
    from the ``_get_empty_metrics()`` structure the method returns from its broad
    ``except`` fallback. Asserting it — rather than ``"total_tokens"``, which is
    present in both — guarantees the period branch ran to completion instead of
    silently crashing into the fallback.
    """

    def _run_with_period(self, period):
        counter = TokenCounter()
        # Provide explicit empty all_sequence so .all() returns [] not MagicMock
        mock_session = _make_session(
            all_sequence=[[], [], [], [], [], [], [], [], [], []],
        )
        with patch("flask.session", {"username": "testuser"}):
            with _patch_get_user_db_session(mock_session):
                return counter._get_metrics_from_encrypted_db(period, "all")

    def test_period_today(self):
        result = self._run_with_period("today")
        assert "rate_limiting" in result

    def test_period_week(self):
        result = self._run_with_period("week")
        assert "rate_limiting" in result

    def test_period_month(self):
        result = self._run_with_period("month")
        assert "rate_limiting" in result

    def test_period_3m(self):
        result = self._run_with_period("3m")
        assert "rate_limiting" in result

    def test_period_1y(self):
        result = self._run_with_period("1y")
        assert "rate_limiting" in result


class TestMetricsRateLimiting:
    """Tests for rate limiting aggregation when data is present."""

    def test_avg_wait_time_with_attempts(self):
        """Average wait times calculated correctly when attempts exist."""
        counter = TokenCounter()

        attempt1 = MagicMock(wait_time=2.0, success=True, engine_type="searxng")
        attempt2 = MagicMock(
            wait_time=4.0, success=False, engine_type="searxng"
        )

        engine_row = MagicMock(engine_type="searxng")

        mock_session = _make_session(
            all_sequence=[
                [],  # 1. sample_researches
                [],  # 2. model_stats
                [],  # 3. recent_research_data
                [attempt1, attempt2],  # 4. rate_limit_attempts
                [engine_row],  # 5. engine_types → triggers estimates
                [],  # 5b. estimates (empty)
            ],
            count_sequence=[2, 1, 0],  # total, successful, rate_limit_events
        )

        with patch("flask.session", {"username": "testuser"}):
            with _patch_get_user_db_session(mock_session):
                result = counter._get_metrics_from_encrypted_db("30d", "all")

        assert result["rate_limiting"]["avg_wait_time"] == 3.0
        assert result["rate_limiting"]["avg_successful_wait"] == 2.0

    def test_engine_stats_with_estimates(self):
        """Engine stats include RateLimitEstimate data when available."""
        counter = TokenCounter()

        attempt = MagicMock(wait_time=1.5, success=True, engine_type="brave")
        engine_row = MagicMock(engine_type="brave")

        estimate = MagicMock()
        estimate.engine_type = "brave"
        estimate.base_wait_seconds = 1.0
        estimate.min_wait_seconds = 0.5
        estimate.max_wait_seconds = 3.0
        estimate.success_rate = 0.95
        estimate.total_attempts = 100
        estimate.last_updated = time.time()

        mock_session = _make_session(
            all_sequence=[
                [],  # 1. sample_researches
                [],  # 2. model_stats
                [],  # 3. recent_research_data
                [attempt],  # 4. rate_limit_attempts
                [engine_row],  # 5. engine_types
                [estimate],  # 5b. estimates
            ],
            count_sequence=[1, 1, 0],
        )

        with patch("flask.session", {"username": "testuser"}):
            with _patch_get_user_db_session(mock_session):
                result = counter._get_metrics_from_encrypted_db("30d", "all")

        stats = result["rate_limiting"]["engine_stats"]
        assert len(stats) >= 1
        brave_stats = [s for s in stats if s["engine"] == "brave"]
        assert len(brave_stats) == 1
        assert brave_stats[0]["base_wait_seconds"] == 1.0
        assert brave_stats[0]["status"] == "healthy"
        assert brave_stats[0]["last_updated"] != "Never"

    def test_engine_stats_without_estimates(self):
        """Engine stats fall back to recent success rate when no estimates."""
        counter = TokenCounter()

        attempt = MagicMock(wait_time=1.0, success=True, engine_type="wiki")
        engine_row = MagicMock(engine_type="wiki")

        mock_session = _make_session(
            all_sequence=[
                [],  # 1. sample_researches
                [],  # 2. model_stats
                [],  # 3. recent_research_data
                [attempt],  # 4. rate_limit_attempts
                [engine_row],  # 5. engine_types
                [],  # 5b. estimates (empty)
            ],
            count_sequence=[1, 1, 0],
        )

        with patch("flask.session", {"username": "testuser"}):
            with _patch_get_user_db_session(mock_session):
                result = counter._get_metrics_from_encrypted_db("30d", "all")

        stats = result["rate_limiting"]["engine_stats"]
        wiki_stats = [s for s in stats if s["engine"] == "wiki"]
        assert len(wiki_stats) == 1
        assert wiki_stats[0]["last_updated"] == "Never"
        assert wiki_stats[0]["status"] == "healthy"

    def test_engine_status_degraded(self):
        """Engine with moderate success rate gets 'degraded' status."""
        counter = TokenCounter()

        attempts = []
        for i in range(3):
            a = MagicMock(wait_time=1.0, success=(i < 2), engine_type="ddg")
            attempts.append(a)

        engine_row = MagicMock(engine_type="ddg")

        mock_session = _make_session(
            all_sequence=[
                [],  # 1. sample_researches
                [],  # 2. model_stats
                [],  # 3. recent_research_data
                attempts,  # 4. rate_limit_attempts
                [engine_row],  # 5. engine_types
                [],  # 5b. estimates
            ],
            count_sequence=[3, 2, 0],
        )

        with patch("flask.session", {"username": "testuser"}):
            with _patch_get_user_db_session(mock_session):
                result = counter._get_metrics_from_encrypted_db("30d", "all")

        stats = result["rate_limiting"]["engine_stats"]
        ddg_stats = [s for s in stats if s["engine"] == "ddg"]
        assert len(ddg_stats) == 1
        assert ddg_stats[0]["status"] == "degraded"


class TestSampleResearchDebugLogging:
    """Tests for sample research debug logging branches."""

    def test_sample_researches_present(self):
        """When sample researches exist, debug logging path is exercised."""
        counter = TokenCounter()

        sample = ("id-1", "2026-01-01T00:00:00", "quick")

        mock_session = _make_session(
            all_sequence=[
                [sample],  # 1. sample_researches (non-empty)
                [],  # 2. model_stats
                [],  # 3. recent_research_data
                [],  # 4. rate_limit_attempts
                [],  # 5. engine_types
            ]
        )

        with patch("flask.session", {"username": "testuser"}):
            with _patch_get_user_db_session(mock_session):
                result = counter._get_metrics_from_encrypted_db("30d", "all")

        assert "total_tokens" in result
