"""Tests for AgentConfigResolver — validates config resolution logic."""
from __future__ import annotations

from unittest.mock import AsyncMock

import pytest

from services.shared.agent_config import AgentConfigResolver


def _make_row(
    agent_id: str = "agent-1",
    variant_id: str | None = None,
    model_name: str = "qwen3.5:9b",
    system_prompt: str = "test prompt",
    **kwargs,
) -> dict:
    """Build a mock DB row for the resolver."""
    return {
        "agent_id": agent_id,
        "variant_id": variant_id,
        "model_provider": kwargs.get("model_provider", "ollama"),
        "model_name": model_name,
        "system_prompt": system_prompt,
        "user_prompt_template": kwargs.get("user_prompt_template", ""),
        "prompt_version": kwargs.get("prompt_version", "v1"),
        "temperature": kwargs.get("temperature", 0.0),
        "max_tokens": kwargs.get("max_tokens", 32768),
        "context_window": kwargs.get("context_window", 0),
        "input_token_limit": kwargs.get("input_token_limit", 0),
        "token_budget": kwargs.get("token_budget", 0),
        "timeout_seconds": kwargs.get("timeout_seconds", 120),
        "max_retries": kwargs.get("max_retries", 2),
    }


@pytest.mark.asyncio
async def test_resolve_returns_config_from_db():
    """Resolver returns a ResolvedAgentConfig when the DB has a matching agent."""
    pool = AsyncMock()
    pool.fetchrow = AsyncMock(return_value=_make_row(model_name="qwen3.5:9b-fast"))

    resolver = AgentConfigResolver(pool, ttl_seconds=60)
    config = await resolver.resolve("document-extractor")

    assert config is not None
    assert config.model_name == "qwen3.5:9b-fast"
    assert config.agent_id == "agent-1"
    assert config.variant_id is None


@pytest.mark.asyncio
async def test_resolve_returns_variant_when_active():
    """Resolver returns variant config when an active variant exists."""
    pool = AsyncMock()
    pool.fetchrow = AsyncMock(return_value=_make_row(
        variant_id="variant-1",
        model_name="llama3.1:8b",
        system_prompt="variant prompt",
    ))

    resolver = AgentConfigResolver(pool, ttl_seconds=60)
    config = await resolver.resolve("document-extractor")

    assert config is not None
    assert config.variant_id == "variant-1"
    assert config.model_name == "llama3.1:8b"
    assert config.system_prompt == "variant prompt"


@pytest.mark.asyncio
async def test_resolve_returns_none_when_not_found():
    """Resolver returns None when no agent matches the slug."""
    pool = AsyncMock()
    pool.fetchrow = AsyncMock(return_value=None)

    resolver = AgentConfigResolver(pool, ttl_seconds=60)
    config = await resolver.resolve("nonexistent-agent")

    assert config is None


@pytest.mark.asyncio
async def test_resolve_returns_none_on_db_error():
    """Resolver returns None and doesn't crash when DB query fails."""
    pool = AsyncMock()
    pool.fetchrow = AsyncMock(side_effect=Exception("connection refused"))

    resolver = AgentConfigResolver(pool, ttl_seconds=60)
    config = await resolver.resolve("document-extractor")

    assert config is None


@pytest.mark.asyncio
async def test_resolve_uses_cache_within_ttl():
    """Resolver caches results and doesn't re-query within TTL."""
    pool = AsyncMock()
    pool.fetchrow = AsyncMock(return_value=_make_row(model_name="cached-model"))

    resolver = AgentConfigResolver(pool, ttl_seconds=300)

    config1 = await resolver.resolve("document-extractor")
    config2 = await resolver.resolve("document-extractor")

    assert config1 is not None
    assert config2 is not None
    assert config1.model_name == "cached-model"
    assert config2.model_name == "cached-model"
    # Should only query DB once
    assert pool.fetchrow.call_count == 1


@pytest.mark.asyncio
async def test_resolve_refreshes_after_ttl():
    """Resolver re-queries DB after TTL expires."""
    pool = AsyncMock()
    pool.fetchrow = AsyncMock(return_value=_make_row(model_name="model-v1"))

    resolver = AgentConfigResolver(pool, ttl_seconds=0)  # 0 TTL = always expired

    config1 = await resolver.resolve("document-extractor")
    pool.fetchrow = AsyncMock(return_value=_make_row(model_name="model-v2"))
    config2 = await resolver.resolve("document-extractor")

    assert config1 is not None
    assert config1.model_name == "model-v1"
    assert config2 is not None
    assert config2.model_name == "model-v2"


@pytest.mark.asyncio
async def test_invalidate_clears_cache():
    """invalidate() forces the next resolve to re-query."""
    pool = AsyncMock()
    pool.fetchrow = AsyncMock(return_value=_make_row(model_name="original"))

    resolver = AgentConfigResolver(pool, ttl_seconds=300)

    config1 = await resolver.resolve("document-extractor")
    assert config1 is not None

    resolver.invalidate("document-extractor")
    pool.fetchrow = AsyncMock(return_value=_make_row(model_name="updated"))

    config2 = await resolver.resolve("document-extractor")
    assert config2 is not None
    assert config2.model_name == "updated"


@pytest.mark.asyncio
async def test_invalidate_all_clears_entire_cache():
    """invalidate(None) clears all cached entries."""
    pool = AsyncMock()
    pool.fetchrow = AsyncMock(return_value=_make_row())

    resolver = AgentConfigResolver(pool, ttl_seconds=300)

    await resolver.resolve("agent-a")
    await resolver.resolve("agent-b")
    assert len(resolver._cache) == 2

    resolver.invalidate()
    assert len(resolver._cache) == 0
