"""Tests for runtime/providers."""

from __future__ import annotations

import json
from pathlib import Path

import pytest
from runtime.providers.anthropic_provider import AnthropicProvider, LMStudioProvider, OllamaProvider
from runtime.providers.base import ChatProvider
from runtime.providers.fake import FakeProvider

FIXTURES = Path(__file__).parent.parent / "fixtures"


# ---------------------------------------------------------------------------
# FakeProvider
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_fake_provider_streams_deterministically():
    provider = FakeProvider([["Hello", ", ", "world", "."]])
    chunks = [c async for c in provider.stream_chat(system="s", messages=[])]
    assert chunks == ["Hello", ", ", "world", "."]
    assert "".join(chunks) == "Hello, world."


@pytest.mark.asyncio
async def test_fake_provider_cycles_responses():
    provider = FakeProvider([["First."], ["Second."]])
    first = [c async for c in provider.stream_chat(system="s", messages=[])]
    second = [c async for c in provider.stream_chat(system="s", messages=[])]
    assert "".join(first) == "First."
    assert "".join(second) == "Second."


@pytest.mark.asyncio
async def test_fake_provider_repeats_last_when_exhausted():
    provider = FakeProvider([["Only."]])
    _ = [c async for c in provider.stream_chat(system="s", messages=[])]
    second = [c async for c in provider.stream_chat(system="s", messages=[])]
    assert "".join(second) == "Only."


@pytest.mark.asyncio
async def test_fake_provider_from_jsonl():
    provider = FakeProvider.from_jsonl(FIXTURES / "responses.jsonl")
    first = [c async for c in provider.stream_chat(system="s", messages=[])]
    second = [c async for c in provider.stream_chat(system="s", messages=[])]
    assert "".join(first) == "You ask about virtue."
    assert "".join(second) == "The impediment to action advances action."


def test_fake_provider_raises_on_empty():
    with pytest.raises(ValueError):
        FakeProvider([])


# ---------------------------------------------------------------------------
# Protocol conformance
# ---------------------------------------------------------------------------


def test_fake_provider_satisfies_protocol():
    provider = FakeProvider.single("hi")
    assert isinstance(provider, ChatProvider)


# ---------------------------------------------------------------------------
# AnthropicProvider — shape test via mocked transport
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_anthropic_provider_raises_without_key(monkeypatch):
    monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
    with pytest.raises(ValueError, match="ANTHROPIC_API_KEY"):
        AnthropicProvider()


@pytest.mark.asyncio
async def test_lmstudio_provider_streams_openai_sse():
    import httpx

    async def handler(request: httpx.Request) -> httpx.Response:
        assert request.url == httpx.URL("http://lm.local/v1/chat/completions")
        assert json.loads(request.content)["model"] == "local-model"
        return httpx.Response(
            200,
            content=(
                'data: {"choices":[{"delta":{"content":"Hello"}}]}\n\n'
                'data: {"choices":[{"delta":{"content":" world"}}]}\n\n'
                "data: [DONE]\n\n"
            ),
        )

    provider = LMStudioProvider(
        base_url="http://lm.local/v1",
        model="local-model",
        transport=httpx.MockTransport(handler),
    )
    chunks = [chunk async for chunk in provider.stream_chat(system="s", messages=[])]
    assert chunks == ["Hello", " world"]


# ---------------------------------------------------------------------------
# OllamaProvider — stub raises NotImplementedError
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_ollama_provider_raises_not_implemented():
    provider = OllamaProvider()
    with pytest.raises(NotImplementedError):
        async for _ in provider.stream_chat(system="s", messages=[]):
            pass


# ---------------------------------------------------------------------------
# Provider factory tests
# ---------------------------------------------------------------------------


def test_get_provider_with_fake_provider_env_returns_fake():
    """Test that FAKE_PROVIDER=1 always returns FakeProvider regardless of name."""
    import os
    from runtime.providers.factory import get_provider
    
    # Set fake provider env var
    old_fake = os.environ.get("FAKE_PROVIDER")
    os.environ["FAKE_PROVIDER"] = "1"
    
    try:
        # Test with no name (should return FakeProvider)
        provider = get_provider()
        assert isinstance(provider, FakeProvider)
        
        # Test with explicit name (should still return FakeProvider due to env var)
        provider = get_provider("lmstudio")
        assert isinstance(provider, FakeProvider)
        
        provider = get_provider("anthropic")
        assert isinstance(provider, FakeProvider)
    finally:
        # Restore env var
        if old_fake is not None:
            os.environ["FAKE_PROVIDER"] = old_fake
        elif "FAKE_PROVIDER" in os.environ:
            del os.environ["FAKE_PROVIDER"]


def test_get_provider_with_lmstudio_name_returns_lmstudio(monkeypatch):
    """Test that explicit 'lmstudio' name returns LMStudioProvider."""
    import os
    from runtime.providers.factory import get_provider
    
    # Remove fake provider env var to test actual provider selection
    if "FAKE_PROVIDER" in os.environ:
        monkeypatch.delenv("FAKE_PROVIDER")
    
    # Set up the test environment properly
    provider = get_provider("lmstudio")
    assert isinstance(provider, LMStudioProvider)


def test_get_provider_with_anthropic_name_returns_anthropic(monkeypatch):
    """Test that explicit 'anthropic' name returns AnthropicProvider."""
    import os
    from runtime.providers.factory import get_provider
    
    # Remove fake provider env var to test actual provider selection
    if "FAKE_PROVIDER" in os.environ:
        monkeypatch.delenv("FAKE_PROVIDER")
    
    # Set up the test environment properly
    provider = get_provider("anthropic")
    assert isinstance(provider, AnthropicProvider)


def test_get_provider_with_none_returns_fallback(monkeypatch):
    """Test that None name falls back to LLM_PROVIDER env var."""
    import os
    from runtime.providers.factory import get_provider
    
    # Remove fake provider env var to test actual provider selection
    if "FAKE_PROVIDER" in os.environ:
        monkeypatch.delenv("FAKE_PROVIDER")
    
    # Test with default (anthropic)
    provider = get_provider(None)
    assert isinstance(provider, AnthropicProvider)
    
    # Test with lmstudio env var
    monkeypatch.setenv("LLM_PROVIDER", "lmstudio")
    provider = get_provider(None)
    assert isinstance(provider, LMStudioProvider)