"""Extended tests for rate_limiting/tracker.py - targeting untested core algorithm.

Covers:
- record_outcome() - in-memory tracking and deque initialization
- _update_estimate() - adaptive learning algorithm (all failures, all successes, mixed)
- apply_rate_limit() - actual enforcement via time.sleep
- get_wait_time() - exploration vs exploitation, no context fallback
- _get_quality_status() - threshold classification
"""

import time
from collections import deque
from unittest.mock import patch

from local_deep_research.config.thread_settings import NoSettingsContextError


def _make_tracker(**overrides):
    """Create a tracker in programmatic mode (no DB, no Flask)."""
    with patch(
        "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot"
    ) as mock_gs:
        mock_gs.side_effect = NoSettingsContextError("test")

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.tracker.logger"
        ):
            from local_deep_research.web_search_engines.rate_limiting.tracker import (
                AdaptiveRateLimitTracker,
            )

            defaults = {"programmatic_mode": True}
            defaults.update(overrides)
            tracker = AdaptiveRateLimitTracker(**defaults)

    # Force enable for testing (programmatic_mode defaults to disabled)
    if "enabled" not in overrides:
        tracker.enabled = True

    return tracker


# ── record_outcome ────────────────────────────────────────────────


class TestRecordOutcome:
    """Tests for record_outcome - in-memory tracking (lines 317-382)."""

    def test_disabled_tracker_skips_recording(self):
        """Disabled tracker returns early without recording."""
        tracker = _make_tracker()
        tracker.enabled = False

        tracker.record_outcome("TestEngine", 1.0, True, 1)
        assert "TestEngine" not in tracker.recent_attempts

    def test_first_attempt_initializes_deque(self):
        """First outcome for an engine creates a deque."""
        tracker = _make_tracker()

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.tracker.get_settings_context",
            return_value=None,
        ):
            tracker.record_outcome("NewEngine", 0.5, True, 1)

        assert "NewEngine" in tracker.recent_attempts
        assert isinstance(tracker.recent_attempts["NewEngine"], deque)
        assert len(tracker.recent_attempts["NewEngine"]) == 1

    def test_records_all_fields(self):
        """Recorded attempt contains all expected fields."""
        tracker = _make_tracker()

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.tracker.get_settings_context",
            return_value=None,
        ):
            tracker.record_outcome("Eng", 0.3, True, 2, search_result_count=15)

        attempt = tracker.recent_attempts["Eng"][0]
        assert attempt["wait_time"] == 0.3
        assert attempt["success"] is True
        assert attempt["retry_count"] == 2
        assert attempt["search_result_count"] == 15
        assert "timestamp" in attempt

    def test_multiple_outcomes_append(self):
        """Multiple outcomes are appended to the same deque."""
        tracker = _make_tracker()

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.tracker.get_settings_context",
            return_value=None,
        ):
            for i in range(5):
                tracker.record_outcome("Eng", 0.1 * i, i % 2 == 0, i + 1)

        assert len(tracker.recent_attempts["Eng"]) == 5

    def test_deque_uses_memory_window(self):
        """Deque maxlen is set from memory_window setting."""
        tracker = _make_tracker()
        tracker.memory_window = 3

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.tracker.get_settings_context",
            return_value=None,
        ):
            for i in range(10):
                tracker.record_outcome("Eng", 0.1, True, 1)

        # Only the last 3 should remain
        assert len(tracker.recent_attempts["Eng"]) == 3


# ── _update_estimate ──────────────────────────────────────────────


class TestUpdateEstimate:
    """Tests for _update_estimate - the adaptive learning algorithm (lines 384-516)."""

    def test_skips_with_fewer_than_3_attempts(self):
        """Does not update estimate with fewer than 3 data points."""
        tracker = _make_tracker()
        tracker.recent_attempts["Eng"] = deque(
            [
                {
                    "wait_time": 0.5,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
                {
                    "wait_time": 0.5,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
            ],
            maxlen=100,
        )

        tracker._update_estimate("Eng")
        # No estimate created since < 3 attempts
        assert "Eng" not in tracker.current_estimates

    def test_all_failures_increases_wait(self):
        """All failed attempts → new_base = max(failed_waits) * 1.5, capped at 10s."""
        tracker = _make_tracker()
        tracker.recent_attempts["Eng"] = deque(
            [
                {
                    "wait_time": 1.0,
                    "success": False,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
                {
                    "wait_time": 2.0,
                    "success": False,
                    "timestamp": time.time(),
                    "retry_count": 2,
                    "search_result_count": None,
                },
                {
                    "wait_time": 3.0,
                    "success": False,
                    "timestamp": time.time(),
                    "retry_count": 3,
                    "search_result_count": None,
                },
            ],
            maxlen=100,
        )

        tracker._update_estimate("Eng")

        est = tracker.current_estimates["Eng"]
        # max(failed) = 3.0, * 1.5 = 4.5, capped at 10
        assert est["base"] == 4.5

    def test_all_failures_caps_at_10_seconds(self):
        """DOS protection: base never exceeds 10 seconds."""
        tracker = _make_tracker()
        tracker.recent_attempts["Eng"] = deque(
            [
                {
                    "wait_time": 8.0,
                    "success": False,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
                {
                    "wait_time": 9.0,
                    "success": False,
                    "timestamp": time.time(),
                    "retry_count": 2,
                    "search_result_count": None,
                },
                {
                    "wait_time": 10.0,
                    "success": False,
                    "timestamp": time.time(),
                    "retry_count": 3,
                    "search_result_count": None,
                },
            ],
            maxlen=100,
        )

        tracker._update_estimate("Eng")

        est = tracker.current_estimates["Eng"]
        # max(10) * 1.5 = 15.0, capped at 10
        assert est["base"] == 10.0

    def test_all_successes_uses_median(self):
        """All successes → uses 50th percentile (median) of wait times."""
        tracker = _make_tracker()
        tracker.recent_attempts["Eng"] = deque(
            [
                {
                    "wait_time": 0.1,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
                {
                    "wait_time": 0.2,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
                {
                    "wait_time": 0.3,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
                {
                    "wait_time": 0.4,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
            ],
            maxlen=100,
        )

        tracker._update_estimate("Eng")

        est = tracker.current_estimates["Eng"]
        # Median of sorted [0.1, 0.2, 0.3, 0.4] at index max(0, int(4*0.5)-1) = 1 → 0.2
        assert est["base"] == 0.2

    def test_existing_estimate_uses_learning_rate(self):
        """Existing estimate is updated via exponential moving average."""
        tracker = _make_tracker()
        tracker.learning_rate = 0.5
        tracker.current_estimates["Eng"] = {
            "base": 2.0,
            "min": 0.5,
            "max": 6.0,
            "confidence": 0.5,
        }
        tracker.recent_attempts["Eng"] = deque(
            [
                {
                    "wait_time": 1.0,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
                {
                    "wait_time": 1.0,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
                {
                    "wait_time": 1.0,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
            ],
            maxlen=100,
        )

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.tracker.get_settings_context",
            return_value=None,
        ):
            tracker._update_estimate("Eng")

        est = tracker.current_estimates["Eng"]
        # old_base=2.0, new_median=1.0, learning_rate=0.5
        # new_base = 0.5*2.0 + 0.5*1.0 = 1.5
        assert abs(est["base"] - 1.5) < 0.01

    def test_bounds_calculated_correctly(self):
        """min_wait and max_wait bounds are derived from base."""
        tracker = _make_tracker()
        tracker.recent_attempts["Eng"] = deque(
            [
                {
                    "wait_time": 2.0,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
                {
                    "wait_time": 2.0,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
                {
                    "wait_time": 2.0,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                },
            ],
            maxlen=100,
        )

        tracker._update_estimate("Eng")

        est = tracker.current_estimates["Eng"]
        # base=2.0 → min=max(0.01, 2.0*0.5)=1.0, max=min(10.0, 2.0*3.0)=6.0
        assert est["min"] == 1.0
        assert est["max"] == 6.0

    def test_confidence_increases_with_attempts(self):
        """Confidence = min(attempts/20, 1.0)."""
        tracker = _make_tracker()
        tracker.recent_attempts["Eng"] = deque(
            [
                {
                    "wait_time": 0.5,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                }
                for _ in range(10)
            ],
            maxlen=100,
        )

        tracker._update_estimate("Eng")

        est = tracker.current_estimates["Eng"]
        assert est["confidence"] == 0.5  # 10/20

    def test_programmatic_mode_skips_db_persist(self):
        """In programmatic mode, estimate is not persisted to DB."""
        tracker = _make_tracker()
        tracker.recent_attempts["Eng"] = deque(
            [
                {
                    "wait_time": 0.5,
                    "success": True,
                    "timestamp": time.time(),
                    "retry_count": 1,
                    "search_result_count": None,
                }
                for _ in range(5)
            ],
            maxlen=100,
        )

        # Should not raise even without DB
        tracker._update_estimate("Eng")
        assert "Eng" in tracker.current_estimates


# ── apply_rate_limit ──────────────────────────────────────────────


class TestApplyRateLimit:
    """Tests for apply_rate_limit - enforcement via time.sleep (lines 294-315)."""

    def test_disabled_returns_zero(self):
        """Disabled tracker returns 0.0 without sleeping."""
        tracker = _make_tracker()
        tracker.enabled = False

        with patch("time.sleep") as mock_sleep:
            result = tracker.apply_rate_limit("Eng")

        assert result == 0.0
        mock_sleep.assert_not_called()

    def test_enabled_sleeps_for_wait_time(self):
        """Enabled tracker sleeps for the returned wait time."""
        tracker = _make_tracker()
        tracker.current_estimates["Eng"] = {
            "base": 0.5,
            "min": 0.1,
            "max": 1.0,
            "confidence": 0.8,
        }

        with (
            patch(
                "local_deep_research.web_search_engines.rate_limiting.tracker.get_search_context",
                return_value={"username": "test"},
            ),
            patch("time.sleep") as mock_sleep,
        ):
            result = tracker.apply_rate_limit("Eng")

        assert result > 0
        mock_sleep.assert_called_once()

    def test_zero_wait_skips_sleep(self):
        """Zero wait time doesn't trigger sleep."""
        tracker = _make_tracker()

        with (
            patch(
                "local_deep_research.web_search_engines.rate_limiting.tracker.get_search_context",
                return_value={"username": "test"},
            ),
            patch.object(tracker, "get_wait_time", return_value=0.0),
            patch("time.sleep") as mock_sleep,
        ):
            tracker.apply_rate_limit("Eng")

        mock_sleep.assert_not_called()


# ── get_wait_time: exploration vs exploitation ────────────────────


class TestGetWaitTimeAdvanced:
    """Tests for get_wait_time advanced paths (lines 276-292)."""

    def test_no_context_non_programmatic_returns_zero(self):
        """No user context + not programmatic → returns 0.0 (safety fallback)."""
        tracker = _make_tracker()
        tracker.programmatic_mode = False

        with patch(
            "local_deep_research.web_search_engines.rate_limiting.tracker.get_search_context",
            return_value=None,
        ):
            result = tracker.get_wait_time("Eng")

        assert result == 0.0

    def test_exploration_uses_lower_multiplier(self):
        """During exploration, wait_time = base * uniform(0.5, 0.9)."""
        tracker = _make_tracker()
        tracker.exploration_rate = 1.0  # Always explore
        tracker.current_estimates["Eng"] = {
            "base": 1.0,
            "min": 0.1,
            "max": 5.0,
            "confidence": 0.8,
        }

        with (
            patch(
                "local_deep_research.web_search_engines.rate_limiting.tracker.get_search_context",
                return_value={"username": "test"},
            ),
            patch("random.random", return_value=0.0),
            patch("random.uniform", return_value=0.7),
        ):
            result = tracker.get_wait_time("Eng")

        # base=1.0 * 0.7 = 0.7, bounded by min=0.1, max=5.0
        assert abs(result - 0.7) < 0.01

    def test_exploitation_uses_normal_multiplier(self):
        """During exploitation, wait_time = base * uniform(0.9, 1.1)."""
        tracker = _make_tracker()
        tracker.exploration_rate = 0.0  # Never explore
        tracker.current_estimates["Eng"] = {
            "base": 1.0,
            "min": 0.5,
            "max": 3.0,
            "confidence": 0.8,
        }

        with (
            patch(
                "local_deep_research.web_search_engines.rate_limiting.tracker.get_search_context",
                return_value={"username": "test"},
            ),
            patch("random.random", return_value=0.5),
            patch("random.uniform", return_value=1.0),
        ):
            result = tracker.get_wait_time("Eng")

        assert abs(result - 1.0) < 0.01

    def test_bounds_enforced(self):
        """Wait time is clamped between estimate min and max."""
        tracker = _make_tracker()
        tracker.exploration_rate = 1.0
        tracker.current_estimates["Eng"] = {
            "base": 10.0,
            "min": 2.0,
            "max": 5.0,
            "confidence": 0.8,
        }

        with (
            patch(
                "local_deep_research.web_search_engines.rate_limiting.tracker.get_search_context",
                return_value={"username": "test"},
            ),
            patch("random.random", return_value=0.0),
            patch("random.uniform", return_value=0.5),
        ):
            result = tracker.get_wait_time("Eng")

        # base=10*0.5=5.0, clamped to max=5.0
        assert result == 5.0


# ── _get_quality_status ───────────────────────────────────────────


class TestGetQualityStatus:
    """Tests for _get_quality_status threshold classification (lines 699-710)."""

    def test_critical_below_1(self):
        """Average < 1 result → CRITICAL."""
        tracker = _make_tracker()
        assert tracker._get_quality_status(0.5) == "CRITICAL"

    def test_warning_below_3(self):
        """Average < 3 results → WARNING."""
        tracker = _make_tracker()
        assert tracker._get_quality_status(2.0) == "WARNING"

    def test_caution_below_5(self):
        """Average < 5 results → CAUTION."""
        tracker = _make_tracker()
        assert tracker._get_quality_status(4.0) == "CAUTION"

    def test_good_below_10(self):
        """Average >= 5 but < 10 → GOOD."""
        tracker = _make_tracker()
        assert tracker._get_quality_status(7.0) == "GOOD"

    def test_excellent_10_plus(self):
        """Average >= 10 → EXCELLENT."""
        tracker = _make_tracker()
        assert tracker._get_quality_status(15.0) == "EXCELLENT"
