"""Integration tests: gateway router multi-provider orchestration.

Tests cross-module router behavior with realistic gateway scenarios:
- Provider priority fallback chains
- Local preference override
- Partial provider availability (some up, some down)
- Dynamic availability changes between calls
- Error propagation when all providers fail
- Non-priority providers as last resort
- Concurrent availability checking via list_available
"""

from unittest.mock import AsyncMock

import pytest

from greymatter_core.gateway import CompletionResult, LLMGateway
from greymatter_core.router import GatewayRouter, PROVIDER_PRIORITY


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

def _make_gateway(
    provider: str,
    available: bool = True,
    latency_ms: int = 100,
    tokens: int = 50,
    cost: float = 0.001,
    fail_on_complete: bool = False,
) -> AsyncMock:
    """Create a mock gateway with configurable behavior."""
    gw = AsyncMock(spec=LLMGateway)
    gw.provider = provider
    gw.model_name = f"{provider}-model"
    gw.check_available = AsyncMock(return_value=available)

    if fail_on_complete:
        gw.complete = AsyncMock(side_effect=RuntimeError(f"{provider} completion failed"))
    else:
        gw.complete = AsyncMock(return_value=CompletionResult(
            content=f"Response from {provider}",
            model=f"{provider}-model",
            provider=provider,
            tokens_used=tokens,
            latency_ms=latency_ms,
            cost_estimate=cost,
        ))
    return gw


MESSAGES = [{"role": "user", "content": "Hello, world"}]


# ---------------------------------------------------------------------------
# 1. Provider Priority Fallback
# ---------------------------------------------------------------------------

class TestProviderPriorityFallback:
    """Tests that routing follows the priority chain: claude > openrouter > ollama."""

    @pytest.mark.asyncio
    async def test_routes_to_highest_priority_available(self):
        """When all providers are up, routes to claude (highest priority)."""
        gateways = {
            "claude": _make_gateway("claude"),
            "openrouter": _make_gateway("openrouter"),
            "ollama": _make_gateway("ollama"),
        }
        router = GatewayRouter(gateways)

        result = await router.route(MESSAGES)
        assert result.provider == "claude"

    @pytest.mark.asyncio
    async def test_falls_back_to_openrouter_when_claude_down(self):
        """Claude unavailable → falls back to openrouter."""
        gateways = {
            "claude": _make_gateway("claude", available=False),
            "openrouter": _make_gateway("openrouter"),
            "ollama": _make_gateway("ollama"),
        }
        router = GatewayRouter(gateways)

        result = await router.route(MESSAGES)
        assert result.provider == "openrouter"

    @pytest.mark.asyncio
    async def test_falls_back_to_ollama_when_cloud_down(self):
        """Both claude and openrouter down → falls back to ollama."""
        gateways = {
            "claude": _make_gateway("claude", available=False),
            "openrouter": _make_gateway("openrouter", available=False),
            "ollama": _make_gateway("ollama"),
        }
        router = GatewayRouter(gateways)

        result = await router.route(MESSAGES)
        assert result.provider == "ollama"

    @pytest.mark.asyncio
    async def test_skips_missing_providers_in_priority(self):
        """Only ollama registered → routes to it despite being lowest priority."""
        router = GatewayRouter({"ollama": _make_gateway("ollama")})
        result = await router.route(MESSAGES)
        assert result.provider == "ollama"


# ---------------------------------------------------------------------------
# 2. Local Preference Override
# ---------------------------------------------------------------------------

class TestLocalPreference:
    """Tests prefer='local' routing behavior."""

    @pytest.mark.asyncio
    async def test_prefer_local_uses_ollama(self):
        """prefer='local' routes to ollama even when claude is available."""
        gateways = {
            "claude": _make_gateway("claude"),
            "ollama": _make_gateway("ollama"),
        }
        router = GatewayRouter(gateways)

        result = await router.route(MESSAGES, prefer="local")
        assert result.provider == "ollama"
        # Claude should not have been called
        gateways["claude"].complete.assert_not_called()

    @pytest.mark.asyncio
    async def test_prefer_local_falls_back_when_ollama_down(self):
        """prefer='local' falls back to priority order when ollama is unavailable."""
        gateways = {
            "claude": _make_gateway("claude"),
            "ollama": _make_gateway("ollama", available=False),
        }
        router = GatewayRouter(gateways)

        result = await router.route(MESSAGES, prefer="local")
        assert result.provider == "claude"

    @pytest.mark.asyncio
    async def test_prefer_local_without_ollama_registered(self):
        """prefer='local' when ollama isn't registered falls back to priority."""
        router = GatewayRouter({"claude": _make_gateway("claude")})
        result = await router.route(MESSAGES, prefer="local")
        assert result.provider == "claude"

    @pytest.mark.asyncio
    async def test_prefer_best_uses_priority_order(self):
        """prefer='best' uses standard priority order (same as default)."""
        gateways = {
            "claude": _make_gateway("claude"),
            "ollama": _make_gateway("ollama"),
        }
        router = GatewayRouter(gateways)

        result = await router.route(MESSAGES, prefer="best")
        assert result.provider == "claude"


# ---------------------------------------------------------------------------
# 3. Non-Priority Providers
# ---------------------------------------------------------------------------

class TestNonPriorityProviders:
    """Tests providers not in PROVIDER_PRIORITY as last-resort fallback."""

    @pytest.mark.asyncio
    async def test_custom_provider_as_last_resort(self):
        """A custom provider is used when all priority providers are down."""
        gateways = {
            "claude": _make_gateway("claude", available=False),
            "openrouter": _make_gateway("openrouter", available=False),
            "ollama": _make_gateway("ollama", available=False),
            "custom-llm": _make_gateway("custom-llm"),
        }
        router = GatewayRouter(gateways)

        result = await router.route(MESSAGES)
        assert result.provider == "custom-llm"

    @pytest.mark.asyncio
    async def test_priority_providers_checked_before_custom(self):
        """Priority providers are preferred over custom providers."""
        gateways = {
            "custom-llm": _make_gateway("custom-llm"),
            "ollama": _make_gateway("ollama"),
        }
        router = GatewayRouter(gateways)

        result = await router.route(MESSAGES)
        assert result.provider == "ollama"


# ---------------------------------------------------------------------------
# 4. Dynamic Availability
# ---------------------------------------------------------------------------

class TestDynamicAvailability:
    """Tests routing when provider availability changes between calls."""

    @pytest.mark.asyncio
    async def test_provider_goes_down_between_calls(self):
        """If a provider goes down between calls, routes to next available."""
        claude = _make_gateway("claude")
        ollama = _make_gateway("ollama")

        router = GatewayRouter({"claude": claude, "ollama": ollama})

        # First call: claude is up
        result1 = await router.route(MESSAGES)
        assert result1.provider == "claude"

        # Claude goes down
        claude.check_available = AsyncMock(return_value=False)

        # Second call: falls back to ollama
        result2 = await router.route(MESSAGES)
        assert result2.provider == "ollama"

    @pytest.mark.asyncio
    async def test_provider_comes_back_up(self):
        """Provider that was down can be routed to once available again."""
        claude = _make_gateway("claude", available=False)
        ollama = _make_gateway("ollama")

        router = GatewayRouter({"claude": claude, "ollama": ollama})

        # First call: claude down → ollama
        result1 = await router.route(MESSAGES)
        assert result1.provider == "ollama"

        # Claude comes back
        claude.check_available = AsyncMock(return_value=True)

        result2 = await router.route(MESSAGES)
        assert result2.provider == "claude"


# ---------------------------------------------------------------------------
# 5. Error Propagation
# ---------------------------------------------------------------------------

class TestErrorPropagation:
    """Tests error handling when no providers are available."""

    @pytest.mark.asyncio
    async def test_no_providers_raises_runtime_error(self):
        """All providers down raises RuntimeError."""
        gateways = {
            "claude": _make_gateway("claude", available=False),
            "ollama": _make_gateway("ollama", available=False),
        }
        router = GatewayRouter(gateways)

        with pytest.raises(RuntimeError, match="No LLM providers available"):
            await router.route(MESSAGES)

    @pytest.mark.asyncio
    async def test_empty_gateway_dict_raises(self):
        """Router with no gateways raises RuntimeError."""
        router = GatewayRouter({})

        with pytest.raises(RuntimeError, match="No LLM providers available"):
            await router.route(MESSAGES)


# ---------------------------------------------------------------------------
# 6. list_available
# ---------------------------------------------------------------------------

class TestListAvailable:
    """Tests the list_available method for provider discovery."""

    @pytest.mark.asyncio
    async def test_all_available(self):
        """Lists all providers when all are up."""
        gateways = {
            "claude": _make_gateway("claude"),
            "ollama": _make_gateway("ollama"),
        }
        router = GatewayRouter(gateways)

        available = await router.list_available()
        assert set(available) == {"claude", "ollama"}

    @pytest.mark.asyncio
    async def test_partial_availability(self):
        """Lists only available providers."""
        gateways = {
            "claude": _make_gateway("claude", available=False),
            "openrouter": _make_gateway("openrouter"),
            "ollama": _make_gateway("ollama", available=False),
        }
        router = GatewayRouter(gateways)

        available = await router.list_available()
        assert available == ["openrouter"]

    @pytest.mark.asyncio
    async def test_none_available(self):
        """Returns empty list when no providers are up."""
        gateways = {
            "claude": _make_gateway("claude", available=False),
        }
        router = GatewayRouter(gateways)

        available = await router.list_available()
        assert available == []

    @pytest.mark.asyncio
    async def test_empty_gateways(self):
        """Returns empty list when no gateways registered."""
        router = GatewayRouter({})
        available = await router.list_available()
        assert available == []


# ---------------------------------------------------------------------------
# 7. Kwargs Passthrough
# ---------------------------------------------------------------------------

class TestKwargsPassthrough:
    """Tests that extra kwargs are forwarded to the gateway complete method."""

    @pytest.mark.asyncio
    async def test_kwargs_forwarded_to_complete(self):
        """Extra kwargs like max_tokens are passed through to the gateway."""
        ollama = _make_gateway("ollama")
        router = GatewayRouter({"ollama": ollama})

        await router.route(MESSAGES, max_tokens=1024, temperature=0.7)

        ollama.complete.assert_called_once()
        _, kwargs = ollama.complete.call_args
        assert kwargs["max_tokens"] == 1024
        assert kwargs["temperature"] == 0.7

    @pytest.mark.asyncio
    async def test_messages_forwarded_correctly(self):
        """Messages are passed to the gateway exactly as received."""
        claude = _make_gateway("claude")
        router = GatewayRouter({"claude": claude})

        messages = [
            {"role": "system", "content": "You are helpful."},
            {"role": "user", "content": "What is 2+2?"},
        ]
        await router.route(messages)

        call_args = claude.complete.call_args[0]
        assert call_args[0] == messages
