"""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
from runtime.providers.fallback import FallbackProvider
from runtime.providers.openai_provider import OpenAIProvider

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_openai_provider_streams_response_text(monkeypatch):
    monkeypatch.setenv("OPENAI_API_KEY", "test-key")

    class FakeResponses:
        async def create(self, **kwargs):
            assert kwargs["stream"] is True
            assert kwargs["model"]

            async def events():
                yield type("Event", (), {"type": "response.output_text.delta", "delta": "Hello"})()
                yield type("Event", (), {"type": "response.output_text.delta", "delta": " world"})()

            return events()

    class FakeClient:
        responses = FakeResponses()

    provider = OpenAIProvider(client=FakeClient())
    chunks = [chunk async for chunk in provider.stream_chat(system="s", messages=[])]
    assert chunks == ["Hello", " world"]


@pytest.mark.asyncio
async def test_fallback_provider_retries_only_before_first_output():
    class FailingProvider:
        async def stream_chat(self, **_):
            raise RuntimeError("primary unavailable")
            yield ""  # pragma: no cover

    class WorkingProvider:
        async def stream_chat(self, **_):
            yield "fallback"

    provider = FallbackProvider(FailingProvider(), WorkingProvider())
    chunks = [chunk async for chunk in provider.stream_chat(system="s", messages=[])]
    assert chunks == ["fallback"]


@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
