"""Tests for Claude Quota Monitor and Metabolic Protocol."""

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

from infrastructure.claude_quota import (
    MetabolicTier,
    QuotaMonitor,
    QuotaStatus,
    _time_remaining,
    get_quota_monitor,
)


def _make_status(five_hour: float = 0.0, seven_day: float = 0.0) -> QuotaStatus:
    """Helper: build a QuotaStatus with given utilization values."""
    return QuotaStatus(
        five_hour_utilization=five_hour,
        five_hour_resets_at=None,
        seven_day_utilization=seven_day,
        seven_day_resets_at=None,
        raw_response={},
        fetched_at=datetime.now(UTC),
    )


class TestMetabolicTierThresholds:
    """Test the three-tier metabolic protocol thresholds."""

    def test_burst_when_five_hour_below_50pct(self):
        status = _make_status(five_hour=0.49, seven_day=0.10)
        assert status.recommended_tier == MetabolicTier.BURST

    def test_burst_at_zero_utilization(self):
        status = _make_status(five_hour=0.0, seven_day=0.0)
        assert status.recommended_tier == MetabolicTier.BURST

    def test_active_when_five_hour_at_50pct(self):
        status = _make_status(five_hour=0.50, seven_day=0.10)
        assert status.recommended_tier == MetabolicTier.ACTIVE

    def test_active_when_five_hour_between_50_and_80pct(self):
        status = _make_status(five_hour=0.79, seven_day=0.10)
        assert status.recommended_tier == MetabolicTier.ACTIVE

    def test_active_when_five_hour_at_80pct(self):
        # five_hour >= 0.80 but seven_day < 0.80 → ACTIVE (not RESTING)
        status = _make_status(five_hour=0.80, seven_day=0.50)
        assert status.recommended_tier == MetabolicTier.ACTIVE

    def test_resting_when_seven_day_at_80pct(self):
        status = _make_status(five_hour=0.30, seven_day=0.80)
        assert status.recommended_tier == MetabolicTier.RESTING

    def test_resting_when_seven_day_above_80pct(self):
        status = _make_status(five_hour=0.10, seven_day=0.95)
        assert status.recommended_tier == MetabolicTier.RESTING

    def test_resting_when_both_critical(self):
        status = _make_status(five_hour=0.90, seven_day=0.90)
        assert status.recommended_tier == MetabolicTier.RESTING

    def test_seven_day_takes_precedence_over_five_hour(self):
        # Weekly quota critical overrides whatever five-hour says
        status = _make_status(five_hour=0.10, seven_day=0.85)
        assert status.recommended_tier == MetabolicTier.RESTING


class TestQuotaStatusProperties:
    """Test QuotaStatus computed properties."""

    def test_five_hour_pct(self):
        status = _make_status(five_hour=0.42)
        assert status.five_hour_pct == 42

    def test_seven_day_pct(self):
        status = _make_status(seven_day=0.75)
        assert status.seven_day_pct == 75

    def test_summary_contains_tier(self):
        status = _make_status(five_hour=0.20, seven_day=0.10)
        summary = status.summary()
        assert "burst" in summary
        assert "20%" in summary

    def test_five_hour_resets_in_unknown_when_none(self):
        status = _make_status()
        assert status.five_hour_resets_in == "unknown"

    def test_seven_day_resets_in_unknown_when_none(self):
        status = _make_status()
        assert status.seven_day_resets_in == "unknown"


class TestTimeRemaining:
    """Test _time_remaining helper."""

    def test_none_returns_unknown(self):
        assert _time_remaining(None) == "unknown"

    def test_empty_string_returns_unknown(self):
        assert _time_remaining("") == "unknown"

    def test_past_time_returns_resetting_now(self):
        past = (datetime.now(UTC) - timedelta(hours=1)).isoformat()
        assert _time_remaining(past) == "resetting now"

    def test_future_time_hours_and_minutes(self):
        future = (datetime.now(UTC) + timedelta(hours=2, minutes=15)).isoformat()
        result = _time_remaining(future)
        assert "2h" in result
        # Minutes may vary ±1 due to test execution time
        assert "m" in result

    def test_future_time_minutes_only(self):
        future = (datetime.now(UTC) + timedelta(minutes=45)).isoformat()
        result = _time_remaining(future)
        assert "h" not in result
        # Minutes may vary ±1 due to test execution time
        assert "m" in result

    def test_z_suffix_handled(self):
        future = (datetime.now(UTC) + timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ")
        result = _time_remaining(future)
        assert result != "unknown"


class TestQuotaMonitorSelectModel:
    """Test select_model metabolic routing."""

    def test_no_quota_high_complexity_returns_14b(self):
        monitor = QuotaMonitor()
        monitor._get_token = lambda: None
        assert monitor.select_model("high") == "qwen3:14b"

    def test_no_quota_low_complexity_returns_8b(self):
        monitor = QuotaMonitor()
        monitor._get_token = lambda: None
        assert monitor.select_model("low") == "qwen3:8b"

    def test_burst_tier_high_complexity_returns_cloud(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.10, seven_day=0.10)
        monitor._cache_seconds = 9999
        result = monitor.select_model("high")
        assert result == "claude-sonnet-4-6"

    def test_burst_tier_medium_complexity_returns_14b(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.10, seven_day=0.10)
        monitor._cache_seconds = 9999
        result = monitor.select_model("medium")
        assert result == "qwen3:14b"

    def test_active_tier_returns_14b(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.65, seven_day=0.10)
        monitor._cache_seconds = 9999
        result = monitor.select_model("high")
        assert result == "qwen3:14b"

    def test_resting_tier_returns_8b(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.10, seven_day=0.85)
        monitor._cache_seconds = 9999
        result = monitor.select_model("high")
        assert result == "qwen3:8b"


class TestQuotaMonitorShouldUseCloud:
    """Test should_use_cloud gate."""

    def test_no_credentials_always_false(self):
        monitor = QuotaMonitor()
        monitor._get_token = lambda: None
        assert monitor.should_use_cloud("critical") is False

    def test_critical_task_allowed_when_under_95pct(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.10, seven_day=0.94)
        monitor._cache_seconds = 9999
        assert monitor.should_use_cloud("critical") is True

    def test_critical_task_blocked_when_over_95pct(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.10, seven_day=0.96)
        monitor._cache_seconds = 9999
        assert monitor.should_use_cloud("critical") is False

    def test_high_task_allowed_under_60pct(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.59, seven_day=0.10)
        monitor._cache_seconds = 9999
        assert monitor.should_use_cloud("high") is True

    def test_high_task_blocked_at_60pct(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.60, seven_day=0.10)
        monitor._cache_seconds = 9999
        assert monitor.should_use_cloud("high") is False

    def test_normal_task_allowed_under_30pct(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.29, seven_day=0.10)
        monitor._cache_seconds = 9999
        assert monitor.should_use_cloud("normal") is True

    def test_normal_task_blocked_at_30pct(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.30, seven_day=0.10)
        monitor._cache_seconds = 9999
        assert monitor.should_use_cloud("normal") is False

    def test_routine_task_always_false(self):
        monitor = QuotaMonitor()
        monitor._last_status = _make_status(five_hour=0.0, seven_day=0.0)
        monitor._cache_seconds = 9999
        assert monitor.should_use_cloud("routine") is False


class TestQuotaMonitorCaching:
    """Test 30-second TTL cache."""

    def test_cached_result_returned_within_ttl(self):
        monitor = QuotaMonitor()
        fresh_status = _make_status(five_hour=0.10)
        monitor._last_status = fresh_status
        monitor._cache_seconds = 30

        # Should NOT re-fetch — returns cached
        with patch.object(monitor, "_get_token", return_value="tok") as mock_tok:
            result = monitor.check()
            mock_tok.assert_not_called()

        assert result is fresh_status

    def test_stale_cache_triggers_fetch(self):
        monitor = QuotaMonitor()
        old_time = datetime.now(UTC) - timedelta(seconds=60)
        stale_status = QuotaStatus(
            five_hour_utilization=0.10,
            five_hour_resets_at=None,
            seven_day_utilization=0.10,
            seven_day_resets_at=None,
            raw_response={},
            fetched_at=old_time,
        )
        monitor._last_status = stale_status

        # Token unavailable → returns None (triggers re-fetch path)
        with patch.object(monitor, "_get_token", return_value=None):
            result = monitor.check()

        assert result is None  # No credentials after cache miss


class TestGetQuotaMonitorSingleton:
    """Test module-level singleton."""

    def test_returns_same_instance(self):
        m1 = get_quota_monitor()
        m2 = get_quota_monitor()
        assert m1 is m2

    def test_returns_quota_monitor_instance(self):
        monitor = get_quota_monitor()
        assert isinstance(monitor, QuotaMonitor)
