"""Unit tests for LLM failover (DB-only)."""
import json
import os
import uuid
from datetime import timedelta
from types import SimpleNamespace
from unittest import mock

from django.apps import apps
from django.contrib.auth import get_user_model
from django.test import TestCase, tag, override_settings
from django.utils import timezone

from api.agent.core.llm_config import (
    AgentLLMTier,
    get_llm_config,
    get_llm_config_with_failover,
    get_summarization_llm_configs,
    PROVIDER_CONFIG,
    LLMNotConfiguredError,
    invalidate_llm_bootstrap_cache,
    get_provider_config,
    get_summarization_llm_config,
    get_agent_judge_llm_config,
)
from console.api_views import _build_completion_params
from api.openrouter import DEFAULT_ATTRIBUTION_TITLE
from tests.utils.llm_seed import clear_llm_db, get_intelligence_tier, seed_persistent_basic


@tag("batch_event_llm")
class TestLLMFailover(TestCase):
    def setUp(self):  # noqa: D401
        super().setUp()
        invalidate_llm_bootstrap_cache()

    def _seed_premium_setup(self, include_premium: bool = True):
        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        PersistentTokenRange = apps.get_model('api', 'PersistentTokenRange')
        PersistentLLMTier = apps.get_model('api', 'PersistentLLMTier')
        PersistentTierEndpoint = apps.get_model('api', 'PersistentTierEndpoint')

        provider = LLMProvider.objects.create(
            key='anthropic',
            display_name='Anthropic',
            enabled=True,
            env_var_name='ANTHROPIC_API_KEY',
            browser_backend='ANTHROPIC',
        )
        premium_endpoint = PersistentModelEndpoint.objects.create(
            key='anthropic_premium',
            provider=provider,
            enabled=True,
            litellm_model='anthropic/premium-model',
            supports_tool_choice=True,
        )
        standard_endpoint = PersistentModelEndpoint.objects.create(
            key='anthropic_standard',
            provider=provider,
            enabled=True,
            litellm_model='anthropic/standard-model',
            supports_tool_choice=True,
        )

        token_range = PersistentTokenRange.objects.create(name='default', min_tokens=0, max_tokens=None)
        standard_tier = PersistentLLMTier.objects.create(
            token_range=token_range,
            order=1,
            intelligence_tier=get_intelligence_tier("standard"),
        )
        PersistentTierEndpoint.objects.create(tier=standard_tier, endpoint=standard_endpoint, weight=1.0)

        if include_premium:
            premium_tier = PersistentLLMTier.objects.create(
                token_range=token_range,
                order=1,
                intelligence_tier=get_intelligence_tier("premium"),
            )
            PersistentTierEndpoint.objects.create(tier=premium_tier, endpoint=premium_endpoint, weight=1.0)

        return {
            "premium_endpoint": premium_endpoint,
            "standard_endpoint": standard_endpoint,
        }

    def _make_agent_stub(self, *, plan_id: str = "free", days_since_joined: int | None = 60):
        UserModel = get_user_model()
        user = UserModel.objects.create_user(
            username=f"user-{uuid.uuid4().hex[:8]}",
            email=f"{uuid.uuid4().hex[:8]}@example.com",
            password="test-pass",
        )
        if days_since_joined is not None:
            user.date_joined = timezone.now() - timedelta(days=days_since_joined)
            user.save(update_fields=["date_joined"])

        UserBilling = apps.get_model('api', 'UserBilling')
        billing, created = UserBilling.objects.get_or_create(
            user=user,
            defaults={"subscription": plan_id},
        )
        if not created and billing.subscription != plan_id:
            billing.subscription = plan_id
            billing.save(update_fields=["subscription"])

        return SimpleNamespace(id=uuid.uuid4(), user=user, organization=None)

    def test_simple_config_anthropic_primary(self):
        seed_persistent_basic(include_openrouter=False)
        with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}, clear=True):
            model, params = get_llm_config()
            self.assertEqual(model, "anthropic/claude-sonnet-4-20250514")
            self.assertIn("temperature", params)

    def test_simple_config_google_primary(self):
        # Re-seed with Google-first (adjust weights by reseeding order)
        clear_llm_db()
        seed_persistent_basic(include_openrouter=False)
        # Provide only Google key so Anthropic endpoint is skipped
        with mock.patch.dict(os.environ, {"GOOGLE_API_KEY": "test-key"}, clear=True):
            model, params = get_llm_config()
            self.assertEqual(model, "vertex_ai/gemini-2.5-pro")
            self.assertIn("vertex_project", params)
            self.assertIn("vertex_location", params)

    def test_simple_config_no_providers(self):
        seed_persistent_basic(include_openrouter=False)
        with mock.patch.dict(os.environ, {}, clear=True):
            with self.assertRaises(LLMNotConfiguredError):
                get_llm_config()

    def test_failover_config_includes_all_tier_endpoints(self):
        seed_persistent_basic(include_openrouter=True)
        with mock.patch.dict(os.environ, {
            "ANTHROPIC_API_KEY": "anthropic-key",
            "GOOGLE_API_KEY": "google-key",
            "OPENROUTER_API_KEY": "openrouter-key",
        }, clear=True):
            configs = get_llm_config_with_failover(token_count=200)
            providers = [c[0] for c in configs]  # endpoint keys
            models = [c[1] for c in configs]
            self.assertIn("anthropic_sonnet4", providers)
            self.assertIn("google_gemini_25_pro", providers)
            self.assertIn("openrouter_glm_45", providers)
            self.assertIn("anthropic/claude-sonnet-4-20250514", models)
            self.assertIn("vertex_ai/gemini-2.5-pro", models)
            self.assertIn("openrouter/z-ai/glm-4.5", models)
            self.assertEqual(len(configs), 3)

    def test_low_latency_preference_orders_endpoints(self):
        clear_llm_db()
        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        PersistentTokenRange = apps.get_model('api', 'PersistentTokenRange')
        PersistentLLMTier = apps.get_model('api', 'PersistentLLMTier')
        PersistentTierEndpoint = apps.get_model('api', 'PersistentTierEndpoint')

        provider = LLMProvider.objects.create(
            key='anthropic',
            display_name='Anthropic',
            enabled=True,
            env_var_name='ANTHROPIC_API_KEY',
            browser_backend='ANTHROPIC',
        )
        fast_endpoint = PersistentModelEndpoint.objects.create(
            key='anthropic_fast',
            provider=provider,
            enabled=True,
            litellm_model='anthropic/fast-model',
            supports_tool_choice=True,
            low_latency=True,
        )
        slow_endpoint = PersistentModelEndpoint.objects.create(
            key='anthropic_slow',
            provider=provider,
            enabled=True,
            litellm_model='anthropic/slow-model',
            supports_tool_choice=True,
            low_latency=False,
        )

        token_range = PersistentTokenRange.objects.create(name='default', min_tokens=0, max_tokens=None)
        tier = PersistentLLMTier.objects.create(token_range=token_range, order=1)
        PersistentTierEndpoint.objects.create(tier=tier, endpoint=slow_endpoint, weight=0.5)
        PersistentTierEndpoint.objects.create(tier=tier, endpoint=fast_endpoint, weight=0.5)

        with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test"}, clear=True):
            configs = get_llm_config_with_failover(token_count=0, prefer_low_latency=True)

        endpoint_order = [cfg[0] for cfg in configs]
        self.assertEqual(set(endpoint_order), {fast_endpoint.key, slow_endpoint.key})
        seen_non_low_latency = False
        for endpoint_key in endpoint_order:
            if endpoint_key == slow_endpoint.key:
                seen_non_low_latency = True
            if endpoint_key == fast_endpoint.key:
                self.assertFalse(seen_non_low_latency)

    def test_provider_config_structure(self):
        """Provider config contains expected keys."""
        required_providers = ["anthropic", "google", "openai", "openai_gpt5", "openrouter_glm", "fireworks_qwen3_235b_a22b"]
        for provider in required_providers:
            self.assertIn(provider, PROVIDER_CONFIG)
            config = PROVIDER_CONFIG[provider]
            self.assertIn("env_var", config)
            self.assertIn("model", config)

    def test_failover_with_agent_id_logging(self):
        """Agent ID is properly passed through for logging."""
        seed_persistent_basic(include_openrouter=True)
        with mock.patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}, clear=True):
            configs = get_llm_config_with_failover(agent_id="test-agent-123")
            self.assertTrue(len(configs) >= 1)
            provider, _, _ = configs[0]
            # DB providers are endpoint keys
            self.assertIn(provider, ["openrouter_glm_45", "anthropic_sonnet4", "google_gemini_25_pro"])

    def test_openrouter_configs_include_attribution_headers(self):
        seed_persistent_basic(include_openrouter=True)
        referer = "https://example.com"
        leaky_title = "Gobii Meta Gobii Skill Eval"
        with override_settings(
            PUBLIC_SITE_URL=referer,
            PUBLIC_BRAND_NAME=leaky_title,
        ):
            with mock.patch.dict(os.environ, {"OPENROUTER_API_KEY": "openrouter-key"}, clear=True):
                configs = get_llm_config_with_failover(token_count=12000)
                openrouter_configs = [cfg for cfg in configs if cfg[0] == "openrouter_glm_45"]
                self.assertTrue(openrouter_configs)
                _, _, params = openrouter_configs[0]
                self.assertEqual(
                    params.get("extra_headers"),
                    {"HTTP-Referer": referer, "X-Title": DEFAULT_ATTRIBUTION_TITLE},
                )
                self.assertNotIn("Meta Gobii", params["extra_headers"]["X-Title"])
                self.assertNotIn("Skill Eval", params["extra_headers"]["X-Title"])

    def test_get_provider_config_includes_openrouter_headers(self):
        referer = "https://example.com/app"
        leaky_title = "Gobii Meta Gobii Skill Eval"
        with override_settings(
            PUBLIC_SITE_URL=referer,
            PUBLIC_BRAND_NAME=leaky_title,
        ):
            with mock.patch.dict(os.environ, {"OPENROUTER_API_KEY": "openrouter-key"}, clear=True):
                model, params = get_provider_config("openrouter_glm")
                self.assertEqual(model, "openrouter/z-ai/glm-4.5")
                self.assertEqual(
                    params.get("extra_headers"),
                    {"HTTP-Referer": referer, "X-Title": DEFAULT_ATTRIBUTION_TITLE},
                )
                self.assertNotIn("Meta Gobii", params["extra_headers"]["X-Title"])
                self.assertNotIn("Skill Eval", params["extra_headers"]["X-Title"])

    def test_eval_routing_profile_openrouter_headers_use_generic_public_title(self):
        clear_llm_db()
        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        LLMRoutingProfile = apps.get_model('api', 'LLMRoutingProfile')
        ProfileTokenRange = apps.get_model('api', 'ProfileTokenRange')
        ProfilePersistentTier = apps.get_model('api', 'ProfilePersistentTier')
        ProfilePersistentTierEndpoint = apps.get_model('api', 'ProfilePersistentTierEndpoint')

        provider = LLMProvider.objects.create(
            key='openrouter',
            display_name='OpenRouter',
            enabled=True,
            env_var_name='OPENROUTER_API_KEY',
            browser_backend='OPENAI_COMPAT',
        )
        endpoint = PersistentModelEndpoint.objects.create(
            key='meta_gobii_eval_model',
            provider=provider,
            enabled=True,
            litellm_model='openrouter/z-ai/glm-4.5',
            supports_tool_choice=True,
        )
        profile = LLMRoutingProfile.objects.create(
            name='meta_gobii_skill_eval',
            display_name='Gobii Meta Gobii Skill Eval',
            description='Eval routing profile that must not leak to OpenRouter attribution.',
            is_active=False,
            is_eval_snapshot=True,
        )
        token_range = ProfileTokenRange.objects.create(profile=profile, name='default', min_tokens=0, max_tokens=None)
        tier = ProfilePersistentTier.objects.create(
            token_range=token_range,
            order=1,
            intelligence_tier=get_intelligence_tier("standard"),
        )
        ProfilePersistentTierEndpoint.objects.create(tier=tier, endpoint=endpoint, weight=1.0)

        referer = "https://evals.example.com"
        with override_settings(
            PUBLIC_SITE_URL=referer,
            PUBLIC_BRAND_NAME='Gobii Meta Gobii Skill Eval',
        ):
            with mock.patch.dict(os.environ, {"OPENROUTER_API_KEY": "openrouter-key"}, clear=True):
                configs = get_llm_config_with_failover(routing_profile=profile, token_count=100)

        self.assertEqual(len(configs), 1)
        _provider, _model, params = configs[0]
        self.assertEqual(
            params.get("extra_headers"),
            {"HTTP-Referer": referer, "X-Title": DEFAULT_ATTRIBUTION_TITLE},
        )
        public_headers = json.dumps(params["extra_headers"])
        self.assertNotIn("meta_gobii_skill_eval", public_headers)
        self.assertNotIn("Gobii Meta Gobii Skill Eval", public_headers)

    def test_gpt5_temperature_is_forced(self):
        """Runtime configuration coerces GPT-5 to temperature=1 even without overrides."""

        clear_llm_db()
        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        PersistentTokenRange = apps.get_model('api', 'PersistentTokenRange')
        PersistentLLMTier = apps.get_model('api', 'PersistentLLMTier')
        PersistentTierEndpoint = apps.get_model('api', 'PersistentTierEndpoint')

        provider = LLMProvider.objects.create(
            key='openai',
            display_name='OpenAI',
            enabled=True,
            env_var_name='OPENAI_API_KEY',
            browser_backend='OPENAI',
        )
        endpoint = PersistentModelEndpoint.objects.create(
            key='openai_primary',
            provider=provider,
            enabled=True,
            litellm_model='openai/gpt-5',
            supports_tool_choice=True,
        )

        token_range = PersistentTokenRange.objects.create(name='default', min_tokens=0, max_tokens=None)
        tier = PersistentLLMTier.objects.create(token_range=token_range, order=1)
        PersistentTierEndpoint.objects.create(tier=tier, endpoint=endpoint, weight=1.0)

        with mock.patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test"}, clear=True):
            configs = get_llm_config_with_failover(token_count=0)

        self.assertTrue(configs)
        _, model, params = configs[0]
        self.assertEqual(model, 'openai/gpt-5')
        self.assertEqual(params.get("temperature"), 1.0)

    def test_reasoning_effort_override_respected(self):
        clear_llm_db()
        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        PersistentTokenRange = apps.get_model('api', 'PersistentTokenRange')
        PersistentLLMTier = apps.get_model('api', 'PersistentLLMTier')
        PersistentTierEndpoint = apps.get_model('api', 'PersistentTierEndpoint')

        provider = LLMProvider.objects.create(
            key='openai',
            display_name='OpenAI',
            enabled=True,
            env_var_name='OPENAI_API_KEY',
            browser_backend='OPENAI',
        )
        endpoint = PersistentModelEndpoint.objects.create(
            key='openai_reasoner',
            provider=provider,
            enabled=True,
            litellm_model='openai/gpt-4o-mini',
            supports_tool_choice=True,
            supports_reasoning=True,
            reasoning_effort='low',
        )
        token_range = PersistentTokenRange.objects.create(name='default', min_tokens=0, max_tokens=None)
        tier = PersistentLLMTier.objects.create(token_range=token_range, order=1)
        PersistentTierEndpoint.objects.create(
            tier=tier,
            endpoint=endpoint,
            weight=1.0,
            reasoning_effort_override='high',
        )

        with mock.patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=True):
            configs = get_llm_config_with_failover(token_count=0)

        self.assertTrue(configs)
        _, _, params = configs[0]
        self.assertTrue(params.get("supports_reasoning"))
        self.assertEqual(params.get("reasoning_effort"), "high")

    def test_allow_implied_send_hint_reflects_endpoint_setting(self):
        clear_llm_db()
        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        PersistentTokenRange = apps.get_model('api', 'PersistentTokenRange')
        PersistentLLMTier = apps.get_model('api', 'PersistentLLMTier')
        PersistentTierEndpoint = apps.get_model('api', 'PersistentTierEndpoint')

        provider = LLMProvider.objects.create(
            key='openai',
            display_name='OpenAI',
            enabled=True,
            env_var_name='OPENAI_API_KEY',
            browser_backend='OPENAI',
        )
        endpoint = PersistentModelEndpoint.objects.create(
            key='openai_no_implied_send',
            provider=provider,
            enabled=True,
            litellm_model='openai/gpt-4o-mini',
            supports_tool_choice=True,
            allow_implied_send=False,
        )
        token_range = PersistentTokenRange.objects.create(name='default', min_tokens=0, max_tokens=None)
        tier = PersistentLLMTier.objects.create(token_range=token_range, order=1)
        PersistentTierEndpoint.objects.create(tier=tier, endpoint=endpoint, weight=1.0)

        with mock.patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=True):
            configs = get_llm_config_with_failover(token_count=0)

        self.assertTrue(configs)
        _, _, params = configs[0]
        self.assertFalse(params.get("allow_implied_send"))

    def test_temperature_param_dropped_when_unsupported(self):
        seed_persistent_basic(include_openrouter=False)
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        endpoint = PersistentModelEndpoint.objects.get(key='anthropic_sonnet4')
        endpoint.supports_temperature = False
        endpoint.save(update_fields=["supports_temperature"])

        with mock.patch.dict(os.environ, {
            "ANTHROPIC_API_KEY": "anthropic-key",
            "GOOGLE_API_KEY": "google-key",
        }, clear=True):
            configs = get_llm_config_with_failover(token_count=0)

        target = next((cfg for cfg in configs if cfg[0] == endpoint.key), None)
        self.assertIsNotNone(target)
        _, model, params = target
        self.assertEqual(model, endpoint.litellm_model)
        self.assertNotIn("temperature", params)
        self.assertFalse(params.get("supports_temperature"))

    def test_browser_temperature_param_dropped_when_unsupported(self):
        LLMProvider = apps.get_model('api', 'LLMProvider')
        BrowserModelEndpoint = apps.get_model('api', 'BrowserModelEndpoint')

        provider = LLMProvider.objects.create(
            key='openai',
            display_name='OpenAI',
            enabled=True,
            env_var_name='OPENAI_API_KEY',
            browser_backend='OPENAI',
        )
        endpoint = BrowserModelEndpoint.objects.create(
            key='openai_browser',
            provider=provider,
            enabled=True,
            browser_model='gpt-5.1-codex',
            browser_base_url='https://api.example.com/v1',
            supports_temperature=False,
        )

        with mock.patch.dict(os.environ, {"OPENAI_API_KEY": "sk-browser"}, clear=True):
            model, params = _build_completion_params(
                endpoint,
                provider,
                model_attr="browser_model",
                base_attr="browser_base_url",
                default_temperature=0.5,
                default_max_tokens=64,
            )

        self.assertEqual(model, "openai/gpt-5.1-codex")
        self.assertNotIn("temperature", params)
        self.assertFalse(params.get("supports_temperature"))

    @override_settings(GOBII_PROPRIETARY_MODE=True)
    def test_max_tier_configs_take_priority(self):
        clear_llm_db()
        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        PersistentTokenRange = apps.get_model('api', 'PersistentTokenRange')
        PersistentLLMTier = apps.get_model('api', 'PersistentLLMTier')
        PersistentTierEndpoint = apps.get_model('api', 'PersistentTierEndpoint')

        provider = LLMProvider.objects.create(
            key='openai',
            display_name='OpenAI',
            enabled=True,
            env_var_name='OPENAI_API_KEY',
            browser_backend='OPENAI',
        )
        max_endpoint = PersistentModelEndpoint.objects.create(
            key='openai_max',
            provider=provider,
            enabled=True,
            litellm_model='openai/gpt-5-max',
            supports_tool_choice=True,
        )
        premium_endpoint = PersistentModelEndpoint.objects.create(
            key='openai_premium',
            provider=provider,
            enabled=True,
            litellm_model='openai/gpt-4.1',
            supports_tool_choice=True,
        )
        standard_endpoint = PersistentModelEndpoint.objects.create(
            key='openai_standard',
            provider=provider,
            enabled=True,
            litellm_model='openai/gpt-4o-mini',
            supports_tool_choice=True,
        )

        token_range = PersistentTokenRange.objects.create(name='default', min_tokens=0, max_tokens=None)
        max_tier = PersistentLLMTier.objects.create(
            token_range=token_range,
            order=1,
            intelligence_tier=get_intelligence_tier("max"),
        )
        premium_tier = PersistentLLMTier.objects.create(
            token_range=token_range,
            order=2,
            intelligence_tier=get_intelligence_tier("premium"),
        )
        standard_tier = PersistentLLMTier.objects.create(
            token_range=token_range,
            order=3,
            intelligence_tier=get_intelligence_tier("standard"),
        )
        PersistentTierEndpoint.objects.create(tier=max_tier, endpoint=max_endpoint, weight=1.0)
        PersistentTierEndpoint.objects.create(tier=premium_tier, endpoint=premium_endpoint, weight=1.0)
        PersistentTierEndpoint.objects.create(tier=standard_tier, endpoint=standard_endpoint, weight=1.0)

        with mock.patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test"}, clear=True):
            with mock.patch("api.agent.core.llm_config.get_agent_llm_tier", return_value=AgentLLMTier.MAX):
                configs = get_llm_config_with_failover(token_count=0)

        endpoint_order = [cfg[0] for cfg in configs]
        self.assertEqual(endpoint_order, ["openai_max", "openai_premium", "openai_standard"])

    @override_settings(GOBII_PROPRIETARY_MODE=True)
    def test_premium_tiers_preferred_for_paid_plan(self):
        clear_llm_db()
        seeded = self._seed_premium_setup(include_premium=True)
        agent = self._make_agent_stub(plan_id="startup", days_since_joined=60)

        with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-premium"}, clear=True):
            configs = get_llm_config_with_failover(
                token_count=0,
                agent=agent,
                agent_id=str(agent.id),
            )

        self.assertTrue(configs)
        self.assertEqual(configs[0][0], seeded["premium_endpoint"].key)

    @override_settings(GOBII_PROPRIETARY_MODE=True)
    def test_premium_plan_falls_back_without_premium_tier(self):
        clear_llm_db()
        seeded = self._seed_premium_setup(include_premium=False)
        agent = self._make_agent_stub(plan_id="startup", days_since_joined=45)

        with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-standard"}, clear=True):
            configs = get_llm_config_with_failover(
                token_count=0,
                agent=agent,
                agent_id=str(agent.id),
            )

        self.assertTrue(configs)
        self.assertEqual(configs[0][0], seeded["standard_endpoint"].key)

    @override_settings(GOBII_PROPRIETARY_MODE=True)
    def test_new_free_account_uses_standard_tier(self):
        clear_llm_db()
        seeded = self._seed_premium_setup(include_premium=True)
        agent = self._make_agent_stub(plan_id="free", days_since_joined=5)

        with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-premium"}, clear=True):
            configs = get_llm_config_with_failover(
                token_count=0,
                agent=agent,
                agent_id=str(agent.id),
            )

        self.assertTrue(configs)
        self.assertEqual(configs[0][0], seeded["standard_endpoint"].key)

    @override_settings(GOBII_PROPRIETARY_MODE=True)
    def test_free_user_first_loop_uses_standard_tier(self):
        clear_llm_db()
        seeded = self._seed_premium_setup(include_premium=True)
        agent = self._make_agent_stub(plan_id="free", days_since_joined=90)

        with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-premium"}, clear=True):
            configs = get_llm_config_with_failover(
                token_count=0,
                agent=agent,
                agent_id=str(agent.id),
                is_first_loop=True,
            )

        self.assertTrue(configs)
        self.assertEqual(configs[0][0], seeded["standard_endpoint"].key)

        with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-premium"}, clear=True):
            configs_after = get_llm_config_with_failover(
                token_count=0,
                agent=agent,
                agent_id=str(agent.id),
                is_first_loop=False,
            )

        self.assertTrue(configs_after)
        self.assertEqual(configs_after[0][0], seeded["standard_endpoint"].key)

    @override_settings(GOBII_PROPRIETARY_MODE=True)
    def test_paid_user_first_loop_keeps_standard_preference(self):
        clear_llm_db()
        seeded = self._seed_premium_setup(include_premium=True)
        agent = self._make_agent_stub(plan_id="startup", days_since_joined=90)
        agent.preferred_llm_tier = get_intelligence_tier("standard")

        with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-paid"}, clear=True):
            configs = get_llm_config_with_failover(
                token_count=0,
                agent=agent,
                agent_id=str(agent.id),
                is_first_loop=True,
            )

        self.assertTrue(configs)
        self.assertEqual(configs[0][0], seeded["standard_endpoint"].key)

    @override_settings(GOBII_PROPRIETARY_MODE=True)
    def test_summarization_prefers_premium_tier(self):
        clear_llm_db()
        seeded = self._seed_premium_setup(include_premium=True)
        agent = self._make_agent_stub(plan_id="startup", days_since_joined=60)

        with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-premium"}, clear=True):
            provider, model, params = get_summarization_llm_config(agent=agent)

        self.assertEqual(provider, seeded["premium_endpoint"].key)
        self.assertEqual(model, seeded["premium_endpoint"].litellm_model)
        self.assertIn("temperature", params)

    def test_summarization_uses_profile_override_before_failover(self):
        clear_llm_db()
        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        LLMRoutingProfile = apps.get_model('api', 'LLMRoutingProfile')
        ProfileTokenRange = apps.get_model('api', 'ProfileTokenRange')
        ProfilePersistentTier = apps.get_model('api', 'ProfilePersistentTier')
        ProfilePersistentTierEndpoint = apps.get_model('api', 'ProfilePersistentTierEndpoint')

        provider = LLMProvider.objects.create(
            key='openrouter',
            display_name='OpenRouter',
            enabled=True,
            env_var_name='OPENROUTER_API_KEY',
            browser_backend='OPENAI',
        )
        summary_endpoint = PersistentModelEndpoint.objects.create(
            key='summary_cheap',
            provider=provider,
            enabled=True,
            litellm_model='openrouter/z-ai/glm-4.5-air',
            supports_tool_choice=True,
        )
        fallback_endpoint = PersistentModelEndpoint.objects.create(
            key='fallback_default',
            provider=provider,
            enabled=True,
            litellm_model='openrouter/z-ai/glm-4.5',
            supports_tool_choice=True,
        )
        profile = LLMRoutingProfile.objects.create(
            name='summary-override-test',
            display_name='Summary Override Test',
            is_active=False,
            summarization_endpoint=summary_endpoint,
        )
        token_range = ProfileTokenRange.objects.create(profile=profile, name='default', min_tokens=0, max_tokens=None)
        tier = ProfilePersistentTier.objects.create(
            token_range=token_range,
            order=1,
            intelligence_tier=get_intelligence_tier("standard"),
        )
        ProfilePersistentTierEndpoint.objects.create(tier=tier, endpoint=fallback_endpoint, weight=1.0)

        with mock.patch.dict(os.environ, {"OPENROUTER_API_KEY": "sk-openrouter"}, clear=True):
            configs = get_summarization_llm_configs(routing_profile=profile)
            provider_key, model, _params = get_summarization_llm_config(routing_profile=profile)

        self.assertTrue(configs)
        self.assertEqual(configs[0][0], summary_endpoint.key)
        self.assertEqual(configs[0][1], summary_endpoint.litellm_model)
        self.assertTrue(any(cfg[0] == fallback_endpoint.key for cfg in configs))
        self.assertEqual(provider_key, summary_endpoint.key)
        self.assertEqual(model, summary_endpoint.litellm_model)

    def test_summarization_falls_back_when_override_unusable(self):
        clear_llm_db()
        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        LLMRoutingProfile = apps.get_model('api', 'LLMRoutingProfile')
        ProfileTokenRange = apps.get_model('api', 'ProfileTokenRange')
        ProfilePersistentTier = apps.get_model('api', 'ProfilePersistentTier')
        ProfilePersistentTierEndpoint = apps.get_model('api', 'ProfilePersistentTierEndpoint')

        provider = LLMProvider.objects.create(
            key='anthropic',
            display_name='Anthropic',
            enabled=True,
            env_var_name='ANTHROPIC_API_KEY',
            browser_backend='ANTHROPIC',
        )
        disabled_override = PersistentModelEndpoint.objects.create(
            key='summary_disabled',
            provider=provider,
            enabled=False,
            litellm_model='anthropic/disabled',
            supports_tool_choice=True,
        )
        fallback_endpoint = PersistentModelEndpoint.objects.create(
            key='summary_fallback',
            provider=provider,
            enabled=True,
            litellm_model='anthropic/claude-sonnet-4-20250514',
            supports_tool_choice=True,
        )
        profile = LLMRoutingProfile.objects.create(
            name='summary-fallback-test',
            display_name='Summary Fallback Test',
            is_active=False,
            summarization_endpoint=disabled_override,
        )
        token_range = ProfileTokenRange.objects.create(profile=profile, name='default', min_tokens=0, max_tokens=None)
        tier = ProfilePersistentTier.objects.create(
            token_range=token_range,
            order=1,
            intelligence_tier=get_intelligence_tier("standard"),
        )
        ProfilePersistentTierEndpoint.objects.create(tier=tier, endpoint=fallback_endpoint, weight=1.0)

        with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-anthropic"}, clear=True):
            provider_key, model, _params = get_summarization_llm_config(routing_profile=profile)

        self.assertEqual(provider_key, fallback_endpoint.key)
        self.assertEqual(model, fallback_endpoint.litellm_model)

    def test_agent_judge_uses_dedicated_endpoint_then_highest_regular_tier(self):
        clear_llm_db()
        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')
        LLMRoutingProfile = apps.get_model('api', 'LLMRoutingProfile')
        ProfileTokenRange = apps.get_model('api', 'ProfileTokenRange')
        ProfilePersistentTier = apps.get_model('api', 'ProfilePersistentTier')
        ProfilePersistentTierEndpoint = apps.get_model('api', 'ProfilePersistentTierEndpoint')

        provider = LLMProvider.objects.create(
            key='openrouter',
            display_name='OpenRouter',
            enabled=True,
            env_var_name='OPENROUTER_API_KEY',
            browser_backend='OPENAI',
        )
        judge_endpoint = PersistentModelEndpoint.objects.create(
            key='judge_model',
            provider=provider,
            enabled=True,
            litellm_model='openrouter/openai/gpt-5.1',
            supports_tool_choice=True,
        )
        fallback_endpoint = PersistentModelEndpoint.objects.create(
            key='fallback_default',
            provider=provider,
            enabled=True,
            litellm_model='openrouter/z-ai/glm-4.5',
            supports_tool_choice=True,
        )
        highest_endpoint = PersistentModelEndpoint.objects.create(
            key='fallback_ultra_max',
            provider=provider,
            enabled=True,
            litellm_model='openrouter/openai/gpt-5.1-ultra-max',
            supports_tool_choice=True,
        )
        profile = LLMRoutingProfile.objects.create(
            name='agent-judge-test',
            display_name='Agent Judge Test',
            is_active=False,
            agent_judge_endpoint=judge_endpoint,
        )
        token_range = ProfileTokenRange.objects.create(profile=profile, name='default', min_tokens=0, max_tokens=None)
        tier = ProfilePersistentTier.objects.create(
            token_range=token_range,
            order=1,
            intelligence_tier=get_intelligence_tier("standard"),
        )
        ProfilePersistentTierEndpoint.objects.create(tier=tier, endpoint=fallback_endpoint, weight=1.0)
        highest_tier = ProfilePersistentTier.objects.create(
            token_range=token_range,
            order=2,
            intelligence_tier=get_intelligence_tier("ultra_max"),
        )
        ProfilePersistentTierEndpoint.objects.create(tier=highest_tier, endpoint=highest_endpoint, weight=1.0)

        with mock.patch.dict(os.environ, {"OPENROUTER_API_KEY": "sk-openrouter"}, clear=True):
            provider_key, model, _params = get_agent_judge_llm_config(routing_profile=profile)

        self.assertEqual(provider_key, judge_endpoint.key)
        self.assertEqual(model, judge_endpoint.litellm_model)

        profile.agent_judge_endpoint = None
        profile.save(update_fields=["agent_judge_endpoint", "updated_at"])
        with mock.patch.dict(os.environ, {"OPENROUTER_API_KEY": "sk-openrouter"}, clear=True):
            provider_key, model, _params = get_agent_judge_llm_config(routing_profile=profile)

        self.assertEqual(provider_key, highest_endpoint.key)
        self.assertEqual(model, highest_endpoint.litellm_model)


@tag("batch_event_llm")
class TestTokenBasedTierSelection(TestCase):
    """DB-only selection scenarios that formerly used token-based tiers."""

    def setUp(self):  # noqa: D401
        super().setUp()
        invalidate_llm_bootstrap_cache()

    def test_db_seeded_range_small_has_endpoints(self):
        seed_persistent_basic(include_openrouter=True)
        with mock.patch.dict(os.environ, {
            "ANTHROPIC_API_KEY": "anthropic-key",
            "GOOGLE_API_KEY": "google-key",
            "OPENROUTER_API_KEY": "openrouter-key",
        }, clear=True):
            configs = get_llm_config_with_failover(token_count=2000)
            providers = [c[0] for c in configs]
            self.assertIn("anthropic_sonnet4", providers)
            self.assertIn("google_gemini_25_pro", providers)
            self.assertIn("openrouter_glm_45", providers)

    def test_db_seeded_selection_medium(self):
        seed_persistent_basic(include_openrouter=True)
        with mock.patch.dict(os.environ, {
            "ANTHROPIC_API_KEY": "anthropic-key",
            "GOOGLE_API_KEY": "google-key",
            "OPENROUTER_API_KEY": "openrouter-key",
        }, clear=True):
            configs = get_llm_config_with_failover(token_count=15000)
            providers = [c[0] for c in configs]
            self.assertIn("openrouter_glm_45", providers)
            self.assertIn("anthropic_sonnet4", providers)

    def test_db_seeded_selection_large(self):
        seed_persistent_basic(include_openrouter=True)
        with mock.patch.dict(os.environ, {
            "ANTHROPIC_API_KEY": "anthropic-key",
            "GOOGLE_API_KEY": "google-key",
        }, clear=True):
            configs = get_llm_config_with_failover(token_count=25000)
            providers = [c[0] for c in configs]
            self.assertIn("google_gemini_25_pro", providers)
            self.assertIn("anthropic_sonnet4", providers)

    # Legacy token-based structure tests have been removed in favor of DB-only selection.

    def test_db_seeded_distribution_medium(self):
        seed_persistent_basic(include_openrouter=True)
        with mock.patch.dict(os.environ, {
            "ANTHROPIC_API_KEY": "anthropic-key",
            "GOOGLE_API_KEY": "google-key",
            "OPENROUTER_API_KEY": "openrouter-key",
        }, clear=True):
            counts = {"openrouter_glm_45": 0, "anthropic_sonnet4": 0, "google_gemini_25_pro": 0}
            for _ in range(50):
                configs = get_llm_config_with_failover(token_count=15000)
                first = configs[0][0]
                if first in counts:
                    counts[first] += 1
            self.assertGreater(sum(counts.values()), 0)


    def test_db_seeded_selection_large_includes_google(self):
        seed_persistent_basic(include_openrouter=True)
        with mock.patch.dict(os.environ, {
            "ANTHROPIC_API_KEY": "anthropic-key",
            "GOOGLE_API_KEY": "google-key",
        }, clear=True):
            configs = get_llm_config_with_failover(token_count=25000)
            providers = [c[0] for c in configs]
            self.assertIn("google_gemini_25_pro", providers)

    def test_missing_providers_are_skipped(self):
        seed_persistent_basic(include_openrouter=True)
        with mock.patch.dict(os.environ, {"GOOGLE_API_KEY": "google-key"}, clear=True):
            configs = get_llm_config_with_failover(token_count=2000)
            providers = [c[0] for c in configs]
            self.assertIn("google_gemini_25_pro", providers)

    def test_no_providers_raises(self):
        seed_persistent_basic(include_openrouter=False)
        with mock.patch.dict(os.environ, {}, clear=True):
            with self.assertRaises(LLMNotConfiguredError):
                get_llm_config_with_failover(token_count=2000)

    # provider_tiers override is no longer supported in DB-only selection; removed.

    def test_weighted_distribution_db_seeded(self):
        seed_persistent_basic(include_openrouter=True)
        with mock.patch.dict(os.environ, {
            "ANTHROPIC_API_KEY": "anthropic-key",
            "GOOGLE_API_KEY": "google-key",
            "OPENROUTER_API_KEY": "openrouter-key",
        }, clear=True):
            counts = {"openrouter_glm_45": 0, "anthropic_sonnet4": 0, "google_gemini_25_pro": 0}
            for _ in range(50):
                configs = get_llm_config_with_failover(token_count=15000)
                first = configs[0][0]
                if first in counts:
                    counts[first] += 1
            self.assertGreater(sum(counts.values()), 0)


@tag("batch_event_llm")
class TestMaxInputTokens(TestCase):
    """Tests for max_input_tokens field on PersistentModelEndpoint."""

    def setUp(self):
        super().setUp()
        invalidate_llm_bootstrap_cache()
        from api.agent.core.llm_config import invalidate_min_endpoint_input_tokens_cache
        invalidate_min_endpoint_input_tokens_cache()
        clear_llm_db()

    def tearDown(self):
        from api.agent.core.llm_config import invalidate_min_endpoint_input_tokens_cache
        invalidate_min_endpoint_input_tokens_cache()
        clear_llm_db()
        super().tearDown()

    def test_get_min_endpoint_input_tokens_returns_minimum(self):
        """get_min_endpoint_input_tokens returns the minimum across enabled endpoints."""
        from api.agent.core.llm_config import get_min_endpoint_input_tokens, invalidate_min_endpoint_input_tokens_cache

        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')

        provider = LLMProvider.objects.create(
            key='test_provider',
            display_name='Test Provider',
            enabled=True,
            env_var_name='TEST_API_KEY',
            browser_backend='OPENAI',
        )
        PersistentModelEndpoint.objects.create(
            key='endpoint_32k',
            provider=provider,
            enabled=True,
            litellm_model='test/model-32k',
            max_input_tokens=32000,
        )
        PersistentModelEndpoint.objects.create(
            key='endpoint_128k',
            provider=provider,
            enabled=True,
            litellm_model='test/model-128k',
            max_input_tokens=128000,
        )
        invalidate_min_endpoint_input_tokens_cache()

        result = get_min_endpoint_input_tokens()
        self.assertEqual(result, 32000)

    def test_get_min_endpoint_input_tokens_returns_none_when_no_limits(self):
        """get_min_endpoint_input_tokens returns None when no endpoints have limits."""
        from api.agent.core.llm_config import get_min_endpoint_input_tokens, invalidate_min_endpoint_input_tokens_cache

        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')

        provider = LLMProvider.objects.create(
            key='test_provider',
            display_name='Test Provider',
            enabled=True,
            env_var_name='TEST_API_KEY',
            browser_backend='OPENAI',
        )
        PersistentModelEndpoint.objects.create(
            key='endpoint_auto',
            provider=provider,
            enabled=True,
            litellm_model='test/model-auto',
            max_input_tokens=None,  # No limit
        )
        invalidate_min_endpoint_input_tokens_cache()

        result = get_min_endpoint_input_tokens()
        self.assertIsNone(result)

    def test_get_min_endpoint_input_tokens_ignores_disabled(self):
        """get_min_endpoint_input_tokens ignores disabled endpoints."""
        from api.agent.core.llm_config import get_min_endpoint_input_tokens, invalidate_min_endpoint_input_tokens_cache

        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')

        provider = LLMProvider.objects.create(
            key='test_provider',
            display_name='Test Provider',
            enabled=True,
            env_var_name='TEST_API_KEY',
            browser_backend='OPENAI',
        )
        PersistentModelEndpoint.objects.create(
            key='endpoint_disabled',
            provider=provider,
            enabled=False,  # Disabled
            litellm_model='test/model-disabled',
            max_input_tokens=8000,  # Would be minimum if enabled
        )
        PersistentModelEndpoint.objects.create(
            key='endpoint_enabled',
            provider=provider,
            enabled=True,
            litellm_model='test/model-enabled',
            max_input_tokens=32000,
        )
        invalidate_min_endpoint_input_tokens_cache()

        result = get_min_endpoint_input_tokens()
        self.assertEqual(result, 32000)  # Ignores the disabled 8k endpoint

    def test_get_prompt_token_budget_respects_endpoint_limit(self):
        """get_prompt_token_budget caps budget to endpoint limit minus headroom."""
        from api.agent.core.llm_config import (
            get_min_endpoint_input_tokens,
            invalidate_min_endpoint_input_tokens_cache,
            INPUT_TOKEN_HEADROOM,
        )
        from api.agent.core.prompt_context import get_prompt_token_budget

        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')

        provider = LLMProvider.objects.create(
            key='test_provider',
            display_name='Test Provider',
            enabled=True,
            env_var_name='TEST_API_KEY',
            browser_backend='OPENAI',
        )
        PersistentModelEndpoint.objects.create(
            key='endpoint_32k',
            provider=provider,
            enabled=True,
            litellm_model='test/model-32k',
            max_input_tokens=32000,
        )
        invalidate_min_endpoint_input_tokens_cache()

        # Verify endpoint limit is 32k
        self.assertEqual(get_min_endpoint_input_tokens(), 32000)

        # Budget should be capped to 32000 - 2000 = 30000
        budget = get_prompt_token_budget(None)
        expected_budget = 32000 - INPUT_TOKEN_HEADROOM
        self.assertEqual(budget, expected_budget)

    def test_get_prompt_token_budget_uses_tier_when_no_limit(self):
        """get_prompt_token_budget uses tier budget when no endpoint limits exist."""
        from api.agent.core.llm_config import invalidate_min_endpoint_input_tokens_cache
        from api.agent.core.prompt_context import get_prompt_token_budget
        from api.services.prompt_settings import get_prompt_settings

        LLMProvider = apps.get_model('api', 'LLMProvider')
        PersistentModelEndpoint = apps.get_model('api', 'PersistentModelEndpoint')

        provider = LLMProvider.objects.create(
            key='test_provider',
            display_name='Test Provider',
            enabled=True,
            env_var_name='TEST_API_KEY',
            browser_backend='OPENAI',
        )
        PersistentModelEndpoint.objects.create(
            key='endpoint_auto',
            provider=provider,
            enabled=True,
            litellm_model='test/model-auto',
            max_input_tokens=None,  # No limit
        )
        invalidate_min_endpoint_input_tokens_cache()

        settings = get_prompt_settings()
        budget = get_prompt_token_budget(None)
        # Should use tier budget since no endpoint limit
        self.assertEqual(budget, settings.standard_prompt_token_budget)
