"""Integration tests: full rotation flow with mocked HTTP."""

from __future__ import annotations

import asyncio
import json

import httpx
import respx

from llm_rotator import (
    LLMRotator,
    OpenAIClient,
    RotatorConfig,
    RoutingContext,
)
from llm_rotator._types import Candidate


def _openai_response(content: str = "ok", model: str = "gpt-4o") -> dict:
    return {
        "id": "chatcmpl-1",
        "object": "chat.completion",
        "model": model,
        "choices": [
            {
                "index": 0,
                "message": {"role": "assistant", "content": content},
                "finish_reason": "stop",
            }
        ],
        "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
    }


def _full_config() -> RotatorConfig:
    return RotatorConfig(
        providers=[
            {
                "name": "provider_a",
                "client_type": "openai",
                "priority": 1,
                "base_url": "https://api-a.example.com/v1",
                "model_groups": [
                    {
                        "name": "flagship",
                        "tier": 1,
                        "models": ["gpt-5", "gpt-4o"],
                        "token_quota": {"limit": 1000, "reset": "daily_utc"},
                    },
                    {
                        "name": "mini",
                        "tier": 3,
                        "models": ["gpt-4o-mini"],
                    },
                ],
                "keys": [
                    {"token": "sk-a1", "alias": "a_key1"},
                    {"token": "sk-a2", "alias": "a_key2"},
                ],
            },
            {
                "name": "provider_b",
                "client_type": "openai",
                "priority": 2,
                "base_url": "https://api-b.example.com/v1",
                "models": ["llama-3"],
                "keys": [
                    {"token": "sk-b1", "alias": "b_key1"},
                ],
            },
        ]
    )


class TestFullRotationHappyPath:
    @respx.mock
    async def test_e2e_happy_path(self):
        """Config → LLMRotator → complete() → response from first provider."""
        respx.post("https://api-a.example.com/v1/chat/completions").mock(
            return_value=httpx.Response(200, json=_openai_response("hello from A"))
        )
        config = _full_config()
        client = OpenAIClient()
        rotator = LLMRotator(
            config, clients={"openai": client}
        )

        result = await rotator.complete(
            messages=[{"role": "user", "content": "hi"}]
        )
        assert result.content == "hello from A"

    @respx.mock
    async def test_e2e_full_rotation_chain(self):
        """First provider returns 429 for all keys, second provider succeeds."""
        # Provider A: all calls → 429
        respx.post("https://api-a.example.com/v1/chat/completions").mock(
            return_value=httpx.Response(
                429,
                json={"error": {"message": "rate limit"}},
                headers={"retry-after": "60"},
            )
        )
        # Provider B: success
        respx.post("https://api-b.example.com/v1/chat/completions").mock(
            return_value=httpx.Response(
                200, json=_openai_response("from B", model="llama-3")
            )
        )

        config = _full_config()
        client = OpenAIClient()
        rotator = LLMRotator(config, clients={"openai": client})

        result = await rotator.complete(
            messages=[{"role": "user", "content": "hi"}]
        )
        assert result.content == "from B"
        assert result.model == "llama-3"


class TestFullRotationQuota:
    @respx.mock
    async def test_e2e_quota_triggers_fallback(self):
        """Token quota exhaustion → automatic fallback to next group."""
        call_count = 0

        def side_effect(request):
            nonlocal call_count
            call_count += 1
            body = json.loads(request.content)
            model = body["model"]
            return httpx.Response(
                200, json=_openai_response(f"response {call_count}", model=model)
            )

        respx.post("https://api-a.example.com/v1/chat/completions").mock(
            side_effect=side_effect
        )

        config = RotatorConfig(
            providers=[
                {
                    "name": "provider_a",
                    "client_type": "openai",
                    "priority": 1,
                    "base_url": "https://api-a.example.com/v1",
                    "model_groups": [
                        {
                            "name": "flagship",
                            "tier": 1,
                            "models": ["gpt-5"],
                            "token_quota": {"limit": 20, "reset": "daily_utc"},
                        },
                        {
                            "name": "mini",
                            "tier": 3,
                            "models": ["gpt-4o-mini"],
                        },
                    ],
                    "keys": [{"token": "sk-1", "alias": "key1"}],
                }
            ]
        )
        client = OpenAIClient()
        rotator = LLMRotator(config, clients={"openai": client})

        # First call uses flagship (15 tokens used, quota=20)
        r1 = await rotator.complete(messages=[{"role": "user", "content": "1"}])
        assert r1.model == "gpt-5"

        # Second call: flagship quota exceeded (15 >= 20? no, 15 < 20)
        # Actually usage=15 per call, after 1 call = 15, still under 20
        # After 2nd call: 30, but we check pre_check before call
        r2 = await rotator.complete(messages=[{"role": "user", "content": "2"}])
        # After r1, usage=15. pre_check: 15 < 20 → OK. After r2, usage=30.
        assert r2.model == "gpt-5"

        # Third call: usage=30 >= 20 → blocked → falls to mini
        r3 = await rotator.complete(messages=[{"role": "user", "content": "3"}])
        assert r3.model == "gpt-4o-mini"


class TestFullRotationHooks:
    @respx.mock
    async def test_e2e_hooks_integrated(self):
        """Hook rejects flagship for free tier → request goes to mini."""
        respx.post("https://api-a.example.com/v1/chat/completions").mock(
            side_effect=lambda req: httpx.Response(
                200,
                json=_openai_response(
                    "mini ok", model=json.loads(req.content)["model"]
                ),
            )
        )

        class TierHook:
            async def before_request(self, ctx, candidate: Candidate) -> bool:
                user_tier = ctx.tags.get("user_tier")
                return not (user_tier == "free" and candidate.model_group == "flagship")

        config = _full_config()
        client = OpenAIClient()
        rotator = LLMRotator(config, clients={"openai": client})
        rotator.add_hook(TierHook())

        result = await rotator.complete(
            messages=[{"role": "user", "content": "hi"}],
            routing=RoutingContext(tags={"user_tier": "free"}),
        )
        # Should skip flagship, use mini
        assert result.model == "gpt-4o-mini"


class TestFullRotationConcurrent:
    @respx.mock
    async def test_e2e_concurrent_requests(self):
        """50 parallel complete() calls work correctly."""
        call_count = 0

        def side_effect(request):
            nonlocal call_count
            call_count += 1
            return httpx.Response(200, json=_openai_response(f"resp {call_count}"))

        respx.post("https://api-a.example.com/v1/chat/completions").mock(
            side_effect=side_effect
        )
        # Provider B not needed but mock it to avoid errors
        respx.post("https://api-b.example.com/v1/chat/completions").mock(
            side_effect=side_effect
        )

        config = _full_config()
        client = OpenAIClient()
        rotator = LLMRotator(config, clients={"openai": client})

        results = await asyncio.gather(*[
            rotator.complete(messages=[{"role": "user", "content": f"msg {i}"}])
            for i in range(50)
        ])

        assert len(results) == 50
        assert all(r.content.startswith("resp") for r in results)


class TestFullRotationStream:
    @respx.mock
    async def test_e2e_stream_with_fallback(self):
        """Stream: first provider 429, fallback to second provider stream."""
        # Provider A: 429
        respx.post("https://api-a.example.com/v1/chat/completions").mock(
            return_value=httpx.Response(
                429,
                json={"error": {"message": "rate limit"}},
                headers={"retry-after": "60"},
            )
        )
        # Provider B: stream success
        sse_data = (
            'data: {"choices":[{"delta":{"content":"streamed"}}]}\n\n'
            "data: [DONE]\n\n"
        )
        respx.post("https://api-b.example.com/v1/chat/completions").mock(
            return_value=httpx.Response(
                200,
                content=sse_data,
                headers={"content-type": "text/event-stream"},
            )
        )

        config = _full_config()
        client = OpenAIClient()
        rotator = LLMRotator(config, clients={"openai": client})

        chunks = []
        async for chunk in rotator.stream(
            messages=[{"role": "user", "content": "hi"}]
        ):
            chunks.append(chunk)

        assert any(c.delta == "streamed" for c in chunks)
