"""Tests for Model Layer — Phase 5."""

from __future__ import annotations

import pytest

from cyberbrain26.model_layer import (
    DiscoveryCategory,
    LiteLLMClient,
    LiteLLMConfig,
    LlamaCppClient,
    LlamaCppConfig,
    ModelCapabilities,
    ModelFreeStatus,
    ModelIntelligence,
    ModelRecord,
    ModelRegistry,
    ModelRouter,
    ModelRouterError,
    ModelStatus,
    ModelTrust,
    ModelType,
    OllamaClient,
    OllamaConfig,
    ProviderFreeScore,
    ProviderRecord,
    ProviderRegistry,
    ProviderStatus,
    ProviderTrust,
    RouteStrategy,
)

# ======================================================================
# Model Registry Tests
# ======================================================================


class TestModelRegistry:
    """Tests for ModelRegistry."""

    @pytest.fixture
    def registry(self) -> ModelRegistry:
        return ModelRegistry()

    @pytest.fixture
    def sample_model(self) -> ModelRecord:
        return ModelRecord(
            model_id="test-model",
            provider="test-provider",
            version="1.0",
            family="test-family",
            model_type=ModelType.CHAT,
            context_window=8192,
            capabilities=ModelCapabilities(
                vision=False,
                tool_calling=True,
                structured_output=False,
                reasoning=False,
                coding=True,
                multimodal=False,
            ),
            local_available=False,
            api_available=True,
            free_status=ModelFreeStatus.FREE,
            license="mit",
            max_tokens=4096,
        )

    def test_register_and_get(self, registry: ModelRegistry, sample_model: ModelRecord) -> None:
        registered = registry.register(sample_model)
        assert registered.model_id == "test-model"
        assert registered.provider == "test-provider"

        retrieved = registry.get("test-model")
        assert retrieved is not None
        assert retrieved.model_id == "test-model"

    def test_unregister(self, registry: ModelRegistry, sample_model: ModelRecord) -> None:
        registry.register(sample_model)
        assert registry.unregister("test-model") is True
        assert registry.get("test-model") is None
        assert registry.unregister("nonexistent") is False

    def test_list_by_provider(self, registry: ModelRegistry) -> None:
        models = [
            ModelRecord(
                model_id=f"model-{i}",
                provider="provider-a",
                model_type=ModelType.CHAT,
            )
            for i in range(3)
        ] + [
            ModelRecord(
                model_id=f"model-{i}",
                provider="provider-b",
                model_type=ModelType.CHAT,
            )
            for i in range(3)
        ]
        for m in models:
            registry.register(m)

        provider_a_models = registry.list_by_provider("provider-a")
        assert len(provider_a_models) == 3
        assert all(m.provider == "provider-a" for m in provider_a_models)

    def test_list_by_type(self, registry: ModelRegistry) -> None:
        models = [
            ModelRecord(
                model_id=f"chat-{i}",
                provider="p",
                model_type=ModelType.CHAT,
            )
            for i in range(2)
        ] + [
            ModelRecord(
                model_id=f"embed-{i}",
                provider="p",
                model_type=ModelType.EMBEDDING,
            )
            for i in range(2)
        ]
        for m in models:
            registry.register(m)

        chat_models = registry.list_by_type(ModelType.CHAT)
        assert len(chat_models) == 2
        assert all(m.model_type == ModelType.CHAT for m in chat_models)

    def test_list_by_status(self, registry: ModelRegistry) -> None:
        models = [
            ModelRecord(
                model_id=f"model-{i}",
                provider="p",
                status=ModelStatus.ACTIVE,
            )
            for i in range(2)
        ] + [
            ModelRecord(
                model_id=f"model-{i}",
                provider="p",
                status=ModelStatus.HIDDEN,
            )
            for i in range(2)
        ]
        for m in models:
            registry.register(m)

        active = registry.list_by_status(ModelStatus.ACTIVE)
        assert len(active) == 2
        assert all(m.status == ModelStatus.ACTIVE for m in active)

    def test_list_active(self, registry: ModelRegistry) -> None:
        for i in range(3):
            registry.register(
                ModelRecord(
                    model_id=f"active-{i}",
                    provider="p",
                    status=ModelStatus.ACTIVE,
                )
            )
        for i in range(2):
            registry.register(
                ModelRecord(
                    model_id=f"hidden-{i}",
                    provider="p",
                    status=ModelStatus.HIDDEN,
                )
            )
        active = registry.list_active()
        assert len(active) == 3

    def test_list_local(self, registry: ModelRegistry) -> None:
        models = [
            ModelRecord(
                model_id=f"local-{i}",
                provider="p",
                local_available=True,
            )
            for i in range(2)
        ] + [
            ModelRecord(
                model_id=f"cloud-{i}",
                provider="p",
                local_available=False,
            )
            for i in range(2)
        ]
        for m in models:
            registry.register(m)

        local = registry.list_local()
        assert len(local) == 2
        assert all(m.local_available for m in local)

    def test_list_free(self, registry: ModelRegistry) -> None:
        models = [
            ModelRecord(
                model_id=f"free-{i}",
                provider="p",
                free_status=ModelFreeStatus.FREE,
            )
            for i in range(2)
        ] + [
            ModelRecord(
                model_id=f"paid-{i}",
                provider="p",
                free_status=ModelFreeStatus.PAID,
            )
            for i in range(2)
        ]
        for m in models:
            registry.register(m)

        free = registry.list_free()
        assert len(free) == 2
        assert all(m.is_free for m in free)

    def test_search(self, registry: ModelRegistry) -> None:
        models = [
            ModelRecord(
                model_id="model-a",
                provider="provider-a",
                model_type=ModelType.CHAT,
                status=ModelStatus.ACTIVE,
                local_available=True,
                free_status=ModelFreeStatus.FREE,
            ),
            ModelRecord(
                model_id="model-b",
                provider="provider-b",
                model_type=ModelType.CODE,
                status=ModelStatus.ACTIVE,
                local_available=False,
                free_status=ModelFreeStatus.PAID,
            ),
        ]
        for m in models:
            registry.register(m)

        results = registry.search(provider="provider-a")
        assert len(results) == 1
        assert results[0].model_id == "model-a"

        results = registry.search(local_only=True)
        assert len(results) == 1
        assert results[0].model_id == "model-a"

        results = registry.search(free_only=True)
        assert len(results) == 1
        assert results[0].model_id == "model-a"

    def test_update(self, registry: ModelRegistry, sample_model: ModelRecord) -> None:
        registry.register(sample_model)
        updated = registry.update("test-model", status=ModelStatus.ACTIVE)
        assert updated is not None
        assert updated.status == ModelStatus.ACTIVE

    def test_count(self, registry: ModelRegistry) -> None:
        assert registry.count() == 0
        registry.register(ModelRecord(model_id="m1", provider="p"))
        registry.register(ModelRecord(model_id="m2", provider="p"))
        assert registry.count() == 2

    def test_has(self, registry: ModelRegistry) -> None:
        registry.register(ModelRecord(model_id="m1", provider="p"))
        assert registry.has("m1") is True
        assert registry.has("m2") is False


# ======================================================================
# Provider Registry Tests
# ======================================================================


class TestProviderRegistry:
    """Tests for ProviderRegistry."""

    @pytest.fixture
    def registry(self) -> ProviderRegistry:
        return ProviderRegistry()

    @pytest.fixture
    def sample_provider(self) -> ProviderRecord:
        return ProviderRecord(
            provider="test-provider",
            status=ProviderStatus.ACTIVE,
            free_tier=True,
            api_available=True,
            available_models=["model-a", "model-b"],
            trust=ProviderTrust.OFFICIAL,
            free=True,
            no_card=True,
            official_source=True,
        )

    def test_register_and_get(self, registry: ProviderRegistry, sample_provider: ProviderRecord) -> None:
        registered = registry.register(sample_provider)
        assert registered.provider == "test-provider"

        retrieved = registry.get("test-provider")
        assert retrieved is not None
        assert retrieved.provider == "test-provider"

    def test_unregister(self, registry: ProviderRegistry, sample_provider: ProviderRecord) -> None:
        registry.register(sample_provider)
        assert registry.unregister("test-provider") is True
        assert registry.get("test-provider") is None

    def test_list_active(self, registry: ProviderRegistry) -> None:
        providers = [
            ProviderRecord(provider=f"p{i}", status=ProviderStatus.ACTIVE)
            for i in range(3)
        ]
        for p in providers:
            registry.register(p)
        registry.register(ProviderRecord(provider="inactive", status=ProviderStatus.INACTIVE))

        active = registry.list_active()
        assert len(active) == 3

    def test_list_local(self, registry: ProviderRegistry) -> None:
        providers = [
            ProviderRecord(provider=f"local-{i}", local_availability=True)
            for i in range(2)
        ]
        for p in providers:
            registry.register(p)
        registry.register(ProviderRecord(provider="cloud", local_availability=False))

        local = registry.list_local()
        assert len(local) == 2

    def test_compute_free_scores(self, registry: ProviderRegistry) -> None:
        provider = ProviderRecord(
            provider="good",
            free=True,
            no_card=True,
            official_source=True,
            stability=1.0,
        )
        registry.register(provider)

        scores = registry.compute_free_scores()
        assert scores["good"] == ProviderFreeScore.EXCELLENT

    def test_search(self, registry: ProviderRegistry) -> None:
        providers = [
            ProviderRecord(
                provider=f"p{i}",
                status=ProviderStatus.ACTIVE,
                local_availability=(i % 2 == 0),
                free=(i % 2 == 0),
            )
            for i in range(4)
        ]
        for p in providers:
            registry.register(p)

        results = registry.search(local_only=True)
        assert len(results) == 2

        results = registry.search(free_only=True)
        assert len(results) == 2

        results = registry.search(status=ProviderStatus.ACTIVE)
        assert len(results) == 4


# ======================================================================
# Model Intelligence Tests
# ======================================================================


class TestModelIntelligence:
    """Tests for ModelIntelligence."""

    @pytest.fixture
    def intelligence(self) -> ModelIntelligence:
        return ModelIntelligence()

    @pytest.fixture
    def sample_model(self) -> ModelRecord:
        return ModelRecord(
            model_id="test-model",
            provider="test-provider",
            version="1.0",
            model_type=ModelType.CHAT,
            context_window=8192,
            capabilities=ModelCapabilities(
                vision=False,
                tool_calling=True,
                coding=True,
            ),
            local_available=False,
            api_available=True,
            free_status=ModelFreeStatus.FREE,
            trust=ModelTrust.OFFICIAL,
            status=ModelStatus.ACTIVE,
        )

    def test_normalize(self, intelligence: ModelIntelligence, sample_model: ModelRecord) -> None:
        model = intelligence.normalize(sample_model)
        assert model.model_id == "test-model"
        assert model.provider == "test-provider"

    def test_normalize_lowercase(self, intelligence: ModelIntelligence) -> None:
        model = ModelRecord(
            model_id="TEST-MODEL",
            provider="GOOGLE",
        )
        normalized = intelligence.normalize(model)
        assert normalized.model_id == "test-model"
        assert normalized.provider == "gemini"

    def test_classify_free(self, intelligence: ModelIntelligence) -> None:
        model = ModelRecord(
            model_id="free-model",
            provider="p",
            free_status=ModelFreeStatus.FREE,
            card_required=False,
        )
        category = intelligence.classify(model)
        assert category == DiscoveryCategory.FREE

    def test_classify_free_for_test(self, intelligence: ModelIntelligence) -> None:
        model = ModelRecord(
            model_id="test-model",
            provider="p",
            free_status=ModelFreeStatus.FREE_FOR_TEST,
        )
        category = intelligence.classify(model)
        assert category == DiscoveryCategory.FREE_FOR_TEST

    def test_classify_new(self, intelligence: ModelIntelligence) -> None:
        model = ModelRecord(
            model_id="new-model",
            provider="p",
            free_status=ModelFreeStatus.PAID,
        )
        category = intelligence.classify(model)
        assert category == DiscoveryCategory.NEW

    def test_score_model_free_official(self, intelligence: ModelIntelligence) -> None:
        model = ModelRecord(
            model_id="best-model",
            provider="p",
            free_status=ModelFreeStatus.FREE,
            trust=ModelTrust.OFFICIAL,
            context_window=128000,
            capabilities=ModelCapabilities(
                vision=True,
                tool_calling=True,
                reasoning=True,
                coding=True,
                multimodal=True,
            ),
            status=ModelStatus.ACTIVE,
        )
        score = intelligence.score_model(model)
        assert score > 0.9

    def test_score_model_paid_unofficial(self, intelligence: ModelIntelligence) -> None:
        model = ModelRecord(
            model_id="worst-model",
            provider="p",
            free_status=ModelFreeStatus.PAID,
            trust=ModelTrust.UNOFFICIAL,
            context_window=2000,
            capabilities=ModelCapabilities(),
            status=ModelStatus.HIDDEN,
        )
        score = intelligence.score_model(model)
        assert score < 0.3

    def test_validate_metadata_valid(self, intelligence: ModelIntelligence) -> None:
        model = ModelRecord(
            model_id="valid-model",
            provider="test-provider",
            context_window=8192,
            max_tokens=4096,
        )
        result = intelligence.validate_metadata(model)
        assert result["valid"] is True
        assert result["issues"] == []

    def test_validate_metadata_missing_fields(self, intelligence: ModelIntelligence) -> None:
        model = ModelRecord(
            model_id="",
            provider="",
            context_window=0,
        )
        result = intelligence.validate_metadata(model)
        assert result["valid"] is False
        assert len(result["issues"]) > 0

    def test_deduplicate(self, intelligence: ModelIntelligence) -> None:
        models = [
            ModelRecord(
                model_id="dup",
                provider="p1",
                last_checked=1000.0,
            ),
            ModelRecord(
                model_id="dup",
                provider="p2",
                last_checked=2000.0,
            ),
            ModelRecord(
                model_id="unique",
                provider="p3",
                last_checked=3000.0,
            ),
        ]
        deduped = intelligence.deduplicate(models)
        assert len(deduped) == 2
        assert deduped[0].provider == "p2"


# ======================================================================
# Model Router Tests
# ======================================================================


class TestModelRouter:
    """Tests for ModelRouter."""

    @pytest.fixture
    def router(self) -> ModelRouter:
        return ModelRouter()

    @pytest.fixture
    def setup_models(self, router: ModelRouter) -> None:
        from cyberbrain26.model_layer import (
            ProviderRecord as _PR,
        )
        from cyberbrain26.model_layer import (
            ProviderStatus as _PS,
        )
        from cyberbrain26.model_layer import (
            ProviderTrust as _PT,
        )

        router.provider_registry.register(
            _PR(
                provider="litellm",
                status=_PS.ACTIVE,
                api_available=True,
                trust=_PT.OFFICIAL,
            )
        )
        router.provider_registry.register(
            _PR(
                provider="ollama",
                status=_PS.ACTIVE,
                local_availability=True,
                api_available=True,
                trust=_PT.COMMUNITY,
            )
        )

        router.model_registry.register(
            ModelRecord(
                model_id="gemini-flash",
                provider="litellm",
                status=ModelStatus.ACTIVE,
                local_available=False,
                api_available=True,
                free_status=ModelFreeStatus.FREE,
                trust=ModelTrust.OFFICIAL,
                context_window=128000,
                capabilities=ModelCapabilities(
                    vision=True,
                    tool_calling=True,
                    reasoning=True,
                    coding=True,
                ),
            )
        )
        router.model_registry.register(
            ModelRecord(
                model_id="llama3",
                provider="ollama",
                status=ModelStatus.ACTIVE,
                local_available=True,
                api_available=True,
                free_status=ModelFreeStatus.FREE,
                trust=ModelTrust.COMMUNITY,
                context_window=8192,
                capabilities=ModelCapabilities(
                    tool_calling=True,
                    coding=True,
                ),
            )
        )

    def test_route_cost_optimized(self, router: ModelRouter, setup_models: None) -> None:
        result = router.route(strategy=RouteStrategy.COST_OPTIMIZED)
        assert result.model_id is not None
        assert result.strategy == RouteStrategy.COST_OPTIMIZED

    def test_route_local_first(self, router: ModelRouter, setup_models: None) -> None:
        result = router.route(strategy=RouteStrategy.LOCAL_FIRST)
        assert result.model_id is not None
        assert result.provider == "ollama" or result.fallback

    def test_route_specific_model(self, router: ModelRouter, setup_models: None) -> None:
        result = router.route(model_id="gemini-flash")
        assert result.model_id == "gemini-flash"
        assert result.provider == "litellm"

    def test_route_performance(self, router: ModelRouter, setup_models: None) -> None:
        result = router.route(strategy=RouteStrategy.PERFORMANCE)
        assert result.model_id is not None
        assert result.strategy == RouteStrategy.PERFORMANCE

    def test_route_reliability(self, router: ModelRouter, setup_models: None) -> None:
        result = router.route(strategy=RouteStrategy.RELIABILITY)
        assert result.model_id is not None
        assert result.strategy == RouteStrategy.RELIABILITY

    def test_route_capability(self, router: ModelRouter, setup_models: None) -> None:
        result = router.route(
            strategy=RouteStrategy.CAPABILITY,
            capabilities={"vision": True},
        )
        assert result.model_id is not None
        model = router.model_registry.get(result.model_id)
        assert model is not None
        assert model.capabilities.vision is True

    def test_route_round_robin(self, router: ModelRouter, setup_models: None) -> None:
        result1 = router.route(strategy=RouteStrategy.ROUND_ROBIN)
        result2 = router.route(strategy=RouteStrategy.ROUND_ROBIN)
        assert result1.model_id is not None
        assert result2.model_id is not None

    def test_route_no_candidates(self, router: ModelRouter) -> None:
        with pytest.raises(ModelRouterError):
            router.route()

    def test_circuit_breaker(self, router: ModelRouter, setup_models: None) -> None:
        assert router._check_circuit_breaker("test") is True

        for _ in range(5):
            router._record_failure("test")

        assert router._check_circuit_breaker("test") is False

        assert router.reset_circuit_breaker("test") is True
        assert router._check_circuit_breaker("test") is True

    def test_route_with_fallback(self, router: ModelRouter, setup_models: None) -> None:
        routes = router.route_with_fallback()
        assert len(routes) >= 1
        assert routes[0].model_id is not None

    def test_get_route_history(self, router: ModelRouter, setup_models: None) -> None:
        router.route(strategy=RouteStrategy.COST_OPTIMIZED)
        history = router.get_route_history()
        assert len(history) >= 1


# ======================================================================
# LiteLLM Client Tests
# ======================================================================


class TestLiteLLMClient:
    """Tests for LiteLLMClient."""

    def test_config_defaults(self) -> None:
        config = LiteLLMConfig()
        assert config.api_key is None
        assert config.base_url is None
        assert config.max_retries == 3
        assert config.timeout == 60.0

    def test_config_custom(self) -> None:
        config = LiteLLMConfig(
            api_key="test-key",
            base_url="http://localhost:4100",
            max_retries=5,
            timeout=120.0,
        )
        assert config.api_key == "test-key"
        assert config.base_url == "http://localhost:4100"
        assert config.max_retries == 5
        assert config.timeout == 120.0

    def test_is_available(self) -> None:
        client = LiteLLMClient()
        assert isinstance(client.is_available(), bool)

    def test_health_check(self) -> None:
        client = LiteLLMClient()
        health = client.health_check()
        assert "healthy" in health
        assert "provider" in health


# ======================================================================
# Ollama Client Tests
# ======================================================================


class TestOllamaClient:
    """Tests for OllamaClient."""

    def test_config_defaults(self) -> None:
        config = OllamaConfig()
        assert config.base_url == "http://127.0.0.1:11434"
        assert config.timeout == 60.0
        assert config.verify_ssl is True

    def test_config_custom(self) -> None:
        config = OllamaConfig(
            base_url="http://custom:1234",
            timeout=600.0,
            verify_ssl=False,
        )
        assert config.base_url == "http://custom:1234"
        assert config.timeout == 600.0
        assert config.verify_ssl is False

    def test_is_available(self, monkeypatch) -> None:
        client = OllamaClient()

        response = type("Response", (), {"status_code": 200})()
        monkeypatch.setattr(
            client,
            "_request",
            lambda *args, **kwargs: response,
        )

        assert client.is_available() is True

    def test_health_check(self) -> None:
        client = OllamaClient()
        health = client.health_check()
        assert "healthy" in health


# ======================================================================
# Llama.cpp Client Tests
# ======================================================================


class TestLlamaCppClient:
    """Tests for LlamaCppClient."""

    def test_config_defaults(self) -> None:
        config = LlamaCppConfig()
        assert config.base_url == "http://127.0.0.1:8080"
        assert config.timeout == 600.0
        assert config.n_ctx == 2048
        assert config.n_gpu_layers == 0
        assert config.n_threads == 4

    def test_config_custom(self) -> None:
        config = LlamaCppConfig(
            base_url="http://custom:8080",
            model_path="/models/llama.gguf",
            n_ctx=4096,
            n_gpu_layers=20,
        )
        assert config.base_url == "http://custom:8080"
        assert config.model_path == "/models/llama.gguf"
        assert config.n_ctx == 4096

    def test_is_available(self) -> None:
        client = LlamaCppClient()
        # The availability depends on whether a llama.cpp server is reachable;
        # assert consistency with a direct probe instead of a hardcoded value.
        import httpx

        try:
            resp = httpx.get("http://127.0.0.1:8080/", timeout=2.0)
            expected = resp.status_code == 200
        except Exception:  # noqa: BLE001
            expected = False
        assert client.is_available() is expected

    def test_health_check(self) -> None:
        client = LlamaCppClient()
        health = client.health_check()
        assert "healthy" in health


# ======================================================================
# Model Gateway Tests
# ======================================================================


class TestModelGateway:
    """Tests for ModelGateway."""

    def test_gateway_initialization(self) -> None:
        from cyberbrain26.control_plane.model_gateway import ModelGateway

        gateway = ModelGateway()
        assert gateway is not None
        assert gateway.model_registry is not None
        assert gateway.provider_registry is not None
        assert gateway.router is not None

    def test_gateway_has_builtin_providers(self) -> None:
        from cyberbrain26.control_plane.model_gateway import ModelGateway

        gateway = ModelGateway()
        providers = gateway.list_providers()
        assert len(providers) >= 3

    def test_gateway_list_models(self) -> None:
        from cyberbrain26.control_plane.model_gateway import ModelGateway

        gateway = ModelGateway()
        models = gateway.list_models()
        assert isinstance(models, list)

    def test_gateway_health_check(self) -> None:
        from cyberbrain26.control_plane.model_gateway import ModelGateway

        gateway = ModelGateway()
        health = gateway.health_check()
        assert "providers" in health
        assert "models" in health
        assert "routing" in health

    def test_gateway_is_healthy(self) -> None:
        from cyberbrain26.control_plane.model_gateway import ModelGateway

        gateway = ModelGateway()
        assert isinstance(gateway.is_healthy(), bool)

    def test_gateway_route(self) -> None:
        from cyberbrain26.control_plane.model_gateway import ModelGateway

        gateway = ModelGateway()
        result = gateway.route()
        assert "model_id" in result
        assert "provider" in result
        assert "strategy" in result
        assert "score" in result

    def test_gateway_route_with_fallback(self) -> None:
        from cyberbrain26.control_plane.model_gateway import ModelGateway

        gateway = ModelGateway()
        results = gateway.route_with_fallback()
        assert len(results) >= 1
        assert "model_id" in results[0]

    def test_gateway_route_specific_model(self) -> None:
        from cyberbrain26.control_plane.model_gateway import ModelGateway

        gateway = ModelGateway()
        result = gateway.route(model_id="gemini-flash")
        assert result["model_id"] == "gemini-flash"

    def test_gateway_discover_models(self) -> None:
        from cyberbrain26.control_plane.model_gateway import ModelGateway

        gateway = ModelGateway()
        models = gateway.discover_models()
        assert isinstance(models, list)

    def test_gateway_register_model(self) -> None:
        from cyberbrain26.control_plane.model_gateway import ModelGateway
        from cyberbrain26.model_layer import ModelRecord, ModelType

        gateway = ModelGateway()
        model = ModelRecord(
            model_id="custom-model",
            provider="custom",
            model_type=ModelType.CHAT,
        )
        registered = gateway.register_model(model)
        assert registered.model_id == "custom-model"
        assert gateway.get_model("custom-model") is not None
