"""Tests for ModernBERT Tier 2 classification-based router.

Covers:
- Pydantic model creation (RequestFeatures, Classification, ModelMapping, ModernBERTResult)
- Router initialization (heuristic fallback, ONNX loading, missing model file)
- Main route() entry point (uninitialised, confident, uncertain, no backends, with backends)
- Feature extraction (_extract_features, _extract_text_content)
- Heuristic classification (_classify_heuristic, _heuristic_complexity, _heuristic_domain,
  _heuristic_quality, _heuristic_tier)
- Model mapping (_map_to_model, _pick_fallback)
- Properties (is_ready, is_using_onnx)
"""

from __future__ import annotations

from datetime import datetime  # noqa: F401 -- needed for Pydantic model_rebuild
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

if TYPE_CHECKING:
    from pathlib import Path

import pytest

from llmhosts.proxy.models import UnifiedMessage, UnifiedRequest
from llmhosts.router.models import BackendInfo

# Ensure forward-ref resolution for BackendInfo (datetime)
BackendInfo.model_rebuild()

from llmhosts.router.modernbert import (  # noqa: E402
    Classification,
    ModelMapping,
    ModernBERTResult,
    ModernBERTRouter,
    RequestFeatures,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_request(
    messages: list[tuple[str, str]] | None = None,
    model: str = "auto",
) -> UnifiedRequest:
    """Build a UnifiedRequest from a list of (role, content) tuples."""
    if messages is None:
        messages = [("user", "Hello")]
    return UnifiedRequest(
        model=model,
        messages=[UnifiedMessage(role=role, content=content) for role, content in messages],
    )


def _make_backend(
    backend_type: str = "ollama",
    url: str = "http://localhost:11434",
    models: list[str] | None = None,
    is_local: bool = True,
    is_healthy: bool = True,
) -> BackendInfo:
    """Build a BackendInfo for testing."""
    return BackendInfo(
        backend_type=backend_type,
        url=url,
        models=models or [],
        is_local=is_local,
        is_healthy=is_healthy,
    )


def _make_features(**overrides) -> RequestFeatures:
    """Build RequestFeatures with sensible defaults, overriding as needed."""
    defaults = {
        "estimated_tokens": 50,
        "has_system_prompt": False,
        "system_prompt_length": 0,
        "message_count": 1,
        "has_code_markers": False,
        "code_language_hints": [],
        "complexity_indicators": 0,
        "domain_keywords": {},
        "is_question": False,
        "is_instruction": False,
        "has_examples": False,
        "estimated_output_length": "medium",
    }
    defaults.update(overrides)
    return RequestFeatures(**defaults)


# ============================================================================
# 1. Model creation tests
# ============================================================================


class TestModelCreation:
    """Verify Pydantic models instantiate with correct defaults and values."""

    def test_request_features_defaults(self):
        """RequestFeatures should accept required fields and fill defaults."""
        features = RequestFeatures(
            estimated_tokens=100,
            has_system_prompt=True,
            message_count=3,
            has_code_markers=False,
            complexity_indicators=2,
            is_question=True,
            is_instruction=False,
            has_examples=False,
        )
        assert features.estimated_tokens == 100
        assert features.system_prompt_length == 0  # default
        assert features.code_language_hints == []  # default
        assert features.domain_keywords == {}  # default
        assert features.estimated_output_length == "medium"  # default

    def test_classification_all_fields(self):
        """Classification should store all dimension labels and confidences."""
        cls = Classification(
            complexity="moderate",
            complexity_confidence=0.85,
            domain="coding",
            domain_confidence=0.90,
            quality_required="high",
            quality_confidence=0.82,
            suggested_tier="medium",
            tier_confidence=0.86,
            overall_confidence=0.86,
        )
        assert cls.complexity == "moderate"
        assert cls.domain == "coding"
        assert cls.quality_required == "high"
        assert cls.suggested_tier == "medium"
        assert cls.overall_confidence == 0.86

    def test_model_mapping_defaults(self):
        """ModelMapping should have optional backend and empty alternatives."""
        mapping = ModelMapping(
            recommended_model="llama3.2:3b",
            reasoning="test",
        )
        assert mapping.recommended_backend is None
        assert mapping.alternatives == []
        assert mapping.recommended_model == "llama3.2:3b"

    def test_modernbert_result_with_mapping(self):
        """ModernBERTResult should accept optional mapping."""
        features = _make_features()
        cls = Classification(
            complexity="simple",
            complexity_confidence=0.85,
            domain="chat",
            domain_confidence=0.80,
            quality_required="low",
            quality_confidence=0.88,
            suggested_tier="tiny",
            tier_confidence=0.90,
            overall_confidence=0.85,
        )
        mapping = ModelMapping(
            recommended_model="phi3:mini",
            recommended_backend="ollama",
            alternatives=["tinyllama:1b"],
            reasoning="trivial chat",
        )
        result = ModernBERTResult(
            action="route",
            confidence=0.85,
            classification=cls,
            mapping=mapping,
            features=features,
            reasoning="routed to phi3:mini",
        )
        assert result.action == "route"
        assert result.mapping is not None
        assert result.mapping.recommended_model == "phi3:mini"

        # Without mapping
        result_no_map = ModernBERTResult(
            action="escalate",
            confidence=0.5,
            classification=cls,
            features=features,
            reasoning="uncertain",
        )
        assert result_no_map.mapping is None


# ============================================================================
# 2. initialize() tests
# ============================================================================


class TestInitialize:
    """Verify router initialization with and without ONNX dependencies."""

    @pytest.mark.asyncio
    async def test_initialize_without_onnx(self):
        """When MODERNBERT_AVAILABLE is False, should fallback to heuristic and be ready."""
        with patch("llmhosts.router.modernbert.MODERNBERT_AVAILABLE", False):
            router = ModernBERTRouter()
            await router.initialize()
            assert router.is_ready is True
            assert router.is_using_onnx is False

    @pytest.mark.asyncio
    async def test_initialize_with_onnx_file(self, tmp_path: Path):
        """When ONNX deps exist and model file present, should attempt to load."""
        model_dir = tmp_path / "router-bert"
        model_dir.mkdir()
        onnx_path = model_dir / "model.onnx"
        onnx_path.write_bytes(b"fake-onnx-model-data")

        mock_session = MagicMock()
        with (
            patch("llmhosts.router.modernbert.MODERNBERT_AVAILABLE", True),
            patch("llmhosts.router.modernbert.ort") as mock_ort,
        ):
            mock_ort.SessionOptions.return_value = MagicMock()
            mock_ort.GraphOptimizationLevel.ORT_ENABLE_ALL = 99
            mock_ort.InferenceSession.return_value = mock_session

            router = ModernBERTRouter(model_dir=model_dir)
            await router.initialize()

            assert router.is_ready is True
            assert router.is_using_onnx is True
            mock_ort.InferenceSession.assert_called_once()

    @pytest.mark.asyncio
    async def test_initialize_no_model_file(self, tmp_path: Path):
        """When ONNX deps exist but model file missing, should fallback to heuristic."""
        model_dir = tmp_path / "router-bert-empty"
        model_dir.mkdir()

        with patch("llmhosts.router.modernbert.MODERNBERT_AVAILABLE", True):
            router = ModernBERTRouter(model_dir=model_dir)
            await router.initialize()

            assert router.is_ready is True
            assert router.is_using_onnx is False


# ============================================================================
# 3. route() tests
# ============================================================================


class TestRoute:
    """Verify the main route() entry point."""

    @pytest.mark.asyncio
    async def test_route_not_initialized_escalates(self):
        """When router is not initialised, route() should return escalate."""
        router = ModernBERTRouter()
        # Deliberately skip initialize()
        request = _make_request([("user", "Hello world")])
        result = await router.route(request, [])
        assert result.action == "escalate"
        assert result.confidence == 0.0
        assert "not initialised" in result.reasoning.lower()

    @pytest.mark.asyncio
    async def test_route_confident_routes(self):
        """When classification confidence >= threshold, should return route action."""
        with patch("llmhosts.router.modernbert.MODERNBERT_AVAILABLE", False):
            router = ModernBERTRouter()
            await router.initialize()

        # A coding request with strong signals should produce high confidence
        request = _make_request(
            [
                (
                    "user",
                    "Write a Python function to implement a binary search algorithm. "
                    "```python\ndef binary_search(arr, target):\n    pass\n```",
                )
            ]
        )
        backends = [
            _make_backend(models=["codellama:7b", "deepseek-coder:6.7b", "llama3.2:3b"]),
        ]
        result = await router.route(request, backends)
        assert result.action == "route"
        assert result.confidence >= router.CONFIDENCE_THRESHOLD
        assert result.mapping is not None
        assert result.mapping.recommended_model != "none"

    @pytest.mark.asyncio
    async def test_route_uncertain_escalates(self):
        """When classification confidence < threshold, should escalate."""
        with patch("llmhosts.router.modernbert.MODERNBERT_AVAILABLE", False):
            router = ModernBERTRouter()
            await router.initialize()

        # Force low confidence by patching _classify_heuristic
        low_conf_cls = Classification(
            complexity="moderate",
            complexity_confidence=0.50,
            domain="chat",
            domain_confidence=0.50,
            quality_required="medium",
            quality_confidence=0.50,
            suggested_tier="medium",
            tier_confidence=0.50,
            overall_confidence=0.50,
        )
        with patch.object(router, "_classify_heuristic", return_value=low_conf_cls):
            request = _make_request([("user", "hmm")])
            result = await router.route(request, [])
            assert result.action == "escalate"
            assert result.confidence < router.CONFIDENCE_THRESHOLD

    @pytest.mark.asyncio
    async def test_route_no_backends_fallback(self):
        """When no backends available and classification maps to 'none', should fallback."""
        with patch("llmhosts.router.modernbert.MODERNBERT_AVAILABLE", False):
            router = ModernBERTRouter()
            await router.initialize()

        request = _make_request([("user", "Write Python code for a REST API")])
        result = await router.route(request, [])
        # With no backends, mapping returns "none" -> action = "fallback"
        assert result.action == "fallback"
        assert result.mapping is not None
        assert result.mapping.recommended_model == "none"

    @pytest.mark.asyncio
    async def test_route_with_available_backends(self):
        """route() should map classification to a real model from available backends."""
        with patch("llmhosts.router.modernbert.MODERNBERT_AVAILABLE", False):
            router = ModernBERTRouter()
            await router.initialize()

        request = _make_request(
            [
                ("system", "You are a helpful coding assistant."),
                (
                    "user",
                    "Implement a Python function to merge two sorted lists. ```python\ndef merge(a, b):\n    pass\n```",
                ),
            ]
        )
        backends = [
            _make_backend(
                backend_type="ollama",
                models=["qwen2.5-coder:7b", "llama3.2:8b", "mistral:7b"],
                is_local=True,
            ),
            _make_backend(
                backend_type="openai",
                url="https://api.openai.com/v1",
                models=["gpt-4o", "gpt-4o-mini"],
                is_local=False,
            ),
        ]
        result = await router.route(request, backends)
        assert result.action in ("route", "escalate", "fallback")
        if result.action == "route":
            assert result.mapping is not None
            assert result.mapping.recommended_model != "none"
            assert result.mapping.recommended_backend in ("ollama", "openai")


# ============================================================================
# 4. _extract_features() tests
# ============================================================================


class TestExtractFeatures:
    """Verify feature extraction from UnifiedRequest messages."""

    def _router(self) -> ModernBERTRouter:
        return ModernBERTRouter()

    def test_empty_messages(self):
        """Empty message list should produce zero-token features."""
        router = self._router()
        request = _make_request([("user", "")])
        features = router._extract_features(request)
        assert features.estimated_tokens == 0
        assert features.has_code_markers is False
        assert features.estimated_output_length == "short"

    def test_code_heavy_message(self):
        """Message with code fences and keywords should detect code markers and language hints."""
        router = self._router()
        request = _make_request(
            [
                (
                    "user",
                    "Fix this Python code:\n```python\ndef hello():\n    print('hello')\n```\n"
                    "Also check the ```javascript\nconsole.log('hi');\n```",
                )
            ]
        )
        features = router._extract_features(request)
        assert features.has_code_markers is True
        assert "python" in features.code_language_hints
        assert "javascript" in features.code_language_hints
        assert features.domain_keywords.get("coding", 0) > 0

    def test_question_message(self):
        """Message containing question words should set is_question."""
        router = self._router()
        request = _make_request([("user", "What is the best algorithm for sorting large datasets?")])
        features = router._extract_features(request)
        assert features.is_question is True

    def test_instruction_message(self):
        """Message with imperative verbs should set is_instruction."""
        router = self._router()
        request = _make_request([("user", "Create a REST API endpoint for user registration")])
        features = router._extract_features(request)
        assert features.is_instruction is True

    def test_long_system_prompt(self):
        """System prompt presence and length should be captured."""
        router = self._router()
        long_system = "You are an expert assistant. " * 50  # ~1400 chars
        request = _make_request(
            [
                ("system", long_system),
                ("user", "Help me with this task."),
            ]
        )
        features = router._extract_features(request)
        assert features.has_system_prompt is True
        assert features.system_prompt_length > 500
        assert features.message_count == 2

    def test_multipart_content(self):
        """Multi-part content blocks (vision-style) should extract text parts."""
        router = self._router()
        request = UnifiedRequest(
            model="auto",
            messages=[
                UnifiedMessage(
                    role="user",
                    content=[
                        {"type": "text", "text": "What is in this image?"},
                        {"type": "image_url", "image_url": {"url": "https://example.com/img.png"}},
                    ],
                )
            ],
        )
        features = router._extract_features(request)
        assert features.estimated_tokens > 0
        assert features.is_question is True


# ============================================================================
# 5. _extract_text_content() tests
# ============================================================================


class TestExtractTextContent:
    """Verify static text extraction from various content shapes."""

    def test_none_returns_empty(self):
        assert ModernBERTRouter._extract_text_content(None) == ""

    def test_string_passthrough(self):
        assert ModernBERTRouter._extract_text_content("Hello world") == "Hello world"

    def test_list_of_content_blocks(self):
        blocks = [
            {"type": "text", "text": "First part"},
            {"type": "image_url", "image_url": {"url": "https://example.com/img.png"}},
            {"type": "text", "text": "Second part"},
        ]
        result = ModernBERTRouter._extract_text_content(blocks)
        assert "First part" in result
        assert "Second part" in result
        assert "image_url" not in result


# ============================================================================
# 6. _classify_heuristic() tests
# ============================================================================


class TestClassifyHeuristic:
    """Verify the heuristic classifier produces correct classification dimensions."""

    def _router(self) -> ModernBERTRouter:
        return ModernBERTRouter()

    def test_trivial_input(self):
        """Short message with no markers -> trivial complexity, chat domain."""
        router = self._router()
        features = _make_features(estimated_tokens=10, message_count=1)
        cls = router._classify_heuristic(features)
        assert cls.complexity == "trivial"
        assert cls.domain == "chat"
        assert cls.quality_required == "low"
        assert cls.overall_confidence > 0.0

    def test_moderate_coding_task(self):
        """Medium tokens + code markers + coding keywords -> moderate, coding."""
        router = self._router()
        features = _make_features(
            estimated_tokens=200,
            has_code_markers=True,
            code_language_hints=["python"],
            domain_keywords={"coding": 5},
            is_instruction=True,
            complexity_indicators=1,
        )
        cls = router._classify_heuristic(features)
        assert cls.complexity in ("moderate", "complex")
        assert cls.domain == "coding"
        assert cls.quality_required in ("medium", "high")

    def test_complex_multistep_reasoning(self):
        """High tokens + many complexity indicators + examples -> complex/expert."""
        router = self._router()
        features = _make_features(
            estimated_tokens=800,
            has_system_prompt=True,
            system_prompt_length=600,
            message_count=6,
            complexity_indicators=5,
            domain_keywords={"analysis": 4, "writing": 2},
            is_question=True,
            is_instruction=True,
            has_examples=True,
            estimated_output_length="long",
        )
        cls = router._classify_heuristic(features)
        assert cls.complexity in ("complex", "expert")
        assert cls.quality_required in ("high", "maximum")

    def test_simple_question(self):
        """Short question with minimal signals -> simple complexity."""
        router = self._router()
        # tokens=60 -> 15 points (range 50-149), is_question alone stays at 15
        # Score 15 is in [10, 25) -> "simple"
        features = _make_features(
            estimated_tokens=60,
            message_count=1,
            is_question=True,
            domain_keywords={"chat": 2},
        )
        cls = router._classify_heuristic(features)
        assert cls.complexity == "simple"
        assert cls.domain == "chat"

    def test_expert_code_reasoning_examples(self):
        """Long prompt with code, reasoning, examples -> expert + maximum quality."""
        router = self._router()
        features = _make_features(
            estimated_tokens=2000,
            has_system_prompt=True,
            system_prompt_length=800,
            message_count=8,
            has_code_markers=True,
            code_language_hints=["python", "rust"],
            complexity_indicators=7,
            domain_keywords={"coding": 10, "analysis": 3},
            is_question=True,
            is_instruction=True,
            has_examples=True,
            estimated_output_length="long",
        )
        cls = router._classify_heuristic(features)
        assert cls.complexity == "expert"
        assert cls.domain == "coding"
        assert cls.quality_required == "maximum"
        assert cls.suggested_tier == "frontier"


# ============================================================================
# 7. _heuristic_complexity() tests
# ============================================================================


class TestHeuristicComplexity:
    """Verify complexity scoring logic."""

    def _router(self) -> ModernBERTRouter:
        return ModernBERTRouter()

    def test_trivial_low_tokens_no_indicators(self):
        """Very short, no indicators -> trivial."""
        router = self._router()
        features = _make_features(estimated_tokens=5)
        label, conf = router._heuristic_complexity(features)
        assert label == "trivial"
        assert conf >= 0.50

    def test_moderate_with_code_markers(self):
        """Code markers + moderate tokens should push to moderate."""
        router = self._router()
        features = _make_features(
            estimated_tokens=200,
            has_code_markers=True,
            has_system_prompt=True,
            complexity_indicators=1,
        )
        label, _conf = router._heuristic_complexity(features)
        assert label in ("moderate", "complex")

    def test_complex_many_indicators(self):
        """Many complexity indicator words + long input -> complex or expert."""
        router = self._router()
        features = _make_features(
            estimated_tokens=600,
            complexity_indicators=6,
            has_examples=True,
            message_count=5,
            estimated_output_length="long",
        )
        label, _conf = router._heuristic_complexity(features)
        assert label in ("complex", "expert")

    def test_near_threshold_confidence_reduction(self):
        """Score near a threshold boundary should reduce confidence."""
        router = self._router()
        # Score ~10 (right at trivial/simple boundary): 5 (tokens 20-49) + 5 (system prompt)
        features = _make_features(
            estimated_tokens=30,
            has_system_prompt=True,
        )
        _label, conf = router._heuristic_complexity(features)
        # At score=10, near threshold 10 -> confidence reduced by 0.10
        # The base is 0.85 for "simple", reduced to 0.75
        assert conf <= 0.85


# ============================================================================
# 8. _heuristic_domain() tests
# ============================================================================


class TestHeuristicDomain:
    """Verify domain classification logic."""

    def _router(self) -> ModernBERTRouter:
        return ModernBERTRouter()

    def test_coding_with_code_markers_and_keywords(self):
        """Code markers + coding keywords -> coding domain, high confidence."""
        router = self._router()
        features = _make_features(
            has_code_markers=True,
            code_language_hints=["python"],
            domain_keywords={"coding": 4, "chat": 1},
        )
        domain, conf = router._heuristic_domain(features)
        assert domain == "coding"
        assert conf >= 0.75

    def test_chat_no_keywords(self):
        """No domain keywords -> default to chat with moderate confidence."""
        router = self._router()
        features = _make_features(domain_keywords={})
        domain, conf = router._heuristic_domain(features)
        assert domain == "chat"
        assert conf == 0.65

    def test_analysis_keywords(self):
        """Analysis keywords dominant -> analysis domain."""
        router = self._router()
        features = _make_features(
            domain_keywords={"analysis": 6, "writing": 1},
        )
        domain, conf = router._heuristic_domain(features)
        assert domain == "analysis"
        assert conf >= 0.80

    def test_tie_breaking_order(self):
        """When domains tie, tiebreaker order should prefer coding > analysis > writing."""
        router = self._router()
        features = _make_features(
            domain_keywords={"coding": 3, "analysis": 3},
        )
        domain, _conf = router._heuristic_domain(features)
        assert domain == "coding"  # tiebreaker: coding > analysis


# ============================================================================
# 9. _heuristic_quality() tests
# ============================================================================


class TestHeuristicQuality:
    """Verify quality requirement estimation."""

    def _router(self) -> ModernBERTRouter:
        return ModernBERTRouter()

    def test_low_for_trivial_chat(self):
        """Trivial complexity + chat domain -> low quality."""
        router = self._router()
        features = _make_features(estimated_output_length="short")
        quality, _conf = router._heuristic_quality(features, "trivial", "chat")
        assert quality == "low"

    def test_high_for_complex_coding(self):
        """Complex complexity + coding domain -> high quality."""
        router = self._router()
        features = _make_features(estimated_output_length="medium")
        quality, _conf = router._heuristic_quality(features, "complex", "coding")
        assert quality == "high"

    def test_maximum_for_expert_with_examples(self):
        """Expert complexity + coding + examples + long system prompt + long output -> maximum."""
        router = self._router()
        features = _make_features(
            has_examples=True,
            system_prompt_length=500,
            estimated_output_length="long",
        )
        quality, _conf = router._heuristic_quality(features, "expert", "coding")
        assert quality == "maximum"


# ============================================================================
# 10. _heuristic_tier() tests
# ============================================================================


class TestHeuristicTier:
    """Verify tier lookup table mapping."""

    def test_known_combinations(self):
        """Known (complexity, quality) pairs should return expected tiers."""
        assert ModernBERTRouter._heuristic_tier("trivial", "low") == ("tiny", 0.92)
        assert ModernBERTRouter._heuristic_tier("simple", "medium") == ("small", 0.86)
        assert ModernBERTRouter._heuristic_tier("moderate", "high") == ("medium", 0.84)
        assert ModernBERTRouter._heuristic_tier("complex", "maximum") == ("frontier", 0.80)
        assert ModernBERTRouter._heuristic_tier("expert", "maximum") == ("frontier", 0.88)

    def test_unknown_combination_fallback(self):
        """Unknown (complexity, quality) pairs should return medium with 0.70 confidence."""
        tier, conf = ModernBERTRouter._heuristic_tier("unknown_complexity", "unknown_quality")
        assert tier == "medium"
        assert conf == 0.70


# ============================================================================
# 11. _map_to_model() tests
# ============================================================================


class TestMapToModel:
    """Verify model mapping from classification to available backends."""

    def _router(self) -> ModernBERTRouter:
        return ModernBERTRouter()

    def _make_classification(
        self,
        complexity: str = "moderate",
        domain: str = "coding",
        quality: str = "medium",
        tier: str = "medium",
    ) -> Classification:
        return Classification(
            complexity=complexity,
            complexity_confidence=0.85,
            domain=domain,
            domain_confidence=0.85,
            quality_required=quality,
            quality_confidence=0.85,
            suggested_tier=tier,
            tier_confidence=0.85,
            overall_confidence=0.85,
        )

    def test_exact_match(self):
        """When a candidate model is available exactly, should select it."""
        router = self._router()
        cls = self._make_classification(complexity="moderate", domain="coding")
        backends = [
            _make_backend(models=["codellama:7b", "llama3.2:8b"]),
        ]
        mapping = router._map_to_model(cls, backends)
        assert mapping.recommended_model != "none"
        # codellama:7b or deepseek-coder:6.7b or qwen2.5-coder:7b are candidates
        # codellama:7b is in both candidate list and available
        assert "codellama" in mapping.recommended_model or "llama" in mapping.recommended_model

    def test_base_name_match(self):
        """When exact candidate not available, base name (before ':') should match."""
        router = self._router()
        cls = self._make_classification(complexity="simple", domain="chat")
        # Candidates include "llama3.2:3b" but we have "llama3.2:3b-q4"
        backends = [
            _make_backend(models=["llama3.2:3b-q4"]),
        ]
        mapping = router._map_to_model(cls, backends)
        assert mapping.recommended_model != "none"
        assert "llama3.2" in mapping.recommended_model

    def test_no_match_uses_fallback(self):
        """When no candidate matches any available model, should fall back."""
        router = self._router()
        cls = self._make_classification(complexity="moderate", domain="coding")
        # Offer only models that are NOT in the candidate list
        backends = [
            _make_backend(models=["some-custom-model:latest"]),
        ]
        mapping = router._map_to_model(cls, backends)
        assert mapping.recommended_model != "none"
        assert "some-custom-model" in mapping.recommended_model
        assert "Falling back" in mapping.reasoning


# ============================================================================
# 12. _pick_fallback() tests
# ============================================================================


class TestPickFallback:
    """Verify fallback model selection heuristics."""

    def test_prefers_local(self):
        """Local backends should be preferred over remote for trivial/simple."""
        cls = Classification(
            complexity="simple",
            complexity_confidence=0.85,
            domain="chat",
            domain_confidence=0.85,
            quality_required="low",
            quality_confidence=0.85,
            suggested_tier="tiny",
            tier_confidence=0.85,
            overall_confidence=0.85,
        )
        backends = [
            _make_backend(
                backend_type="openai",
                url="https://api.openai.com",
                models=["gpt-4o"],
                is_local=False,
            ),
            _make_backend(
                backend_type="ollama",
                url="http://localhost:11434",
                models=["llama3.2:3b"],
                is_local=True,
            ),
        ]
        model, backend = ModernBERTRouter._pick_fallback(backends, cls)
        assert backend == "ollama"
        assert model == "llama3.2:3b"

    def test_complex_prefers_larger(self):
        """For complex tasks, sort should reverse to prefer 'larger' model names."""
        cls = Classification(
            complexity="complex",
            complexity_confidence=0.80,
            domain="analysis",
            domain_confidence=0.80,
            quality_required="high",
            quality_confidence=0.80,
            suggested_tier="large",
            tier_confidence=0.80,
            overall_confidence=0.80,
        )
        backends = [
            _make_backend(
                backend_type="ollama",
                models=["tiny-model:1b", "large-model:70b"],
                is_local=True,
            ),
        ]
        model, backend = ModernBERTRouter._pick_fallback(backends, cls)
        # Reversed sort picks alphabetically last for local models
        # "tiny-model:1b" > "large-model:70b" alphabetically reversed
        assert model in ("tiny-model:1b", "large-model:70b")
        # The important thing is the sort reversal happens for complex tasks
        assert backend == "ollama"


# ============================================================================
# 13. Properties tests
# ============================================================================


class TestProperties:
    """Verify is_ready and is_using_onnx properties."""

    def test_is_ready_before_init(self):
        """Before initialize(), is_ready should be False."""
        router = ModernBERTRouter()
        assert router.is_ready is False

    @pytest.mark.asyncio
    async def test_is_ready_after_init(self):
        """After initialize(), is_ready should be True."""
        with patch("llmhosts.router.modernbert.MODERNBERT_AVAILABLE", False):
            router = ModernBERTRouter()
            await router.initialize()
            assert router.is_ready is True
            assert router.is_using_onnx is False
