"""Integration tests V2: JSON config, Anthropic, tool calling, quotas, Redis, LangChain, logging."""

from __future__ import annotations

import json
import logging
from pathlib import Path

import pytest
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

from llm_rotator import LLMRotator, RotatorConfig
from llm_rotator._types import LLMResponse, ToolCall, Usage
from llm_rotator.backends.redis import RedisBackend
from llm_rotator.exceptions import ServerError
from llm_rotator.integrations.langchain import RotatorChatModel
from llm_rotator.quota import QuotaWarning
from tests.conftest import FakeLLMClient

# Re-usable fakeredis import (optional dep)
fakeredis = pytest.importorskip("fakeredis")


# --- Helpers ---


def _response(
    content: str = "ok",
    model: str = "gpt-4o",
    tool_calls: list[ToolCall] | None = None,
) -> LLMResponse:
    return LLMResponse(
        content=content,
        usage=Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15),
        model=model,
        provider="test",
        key_alias="key1",
        tool_calls=tool_calls,
    )


def _write_json_config(data: dict, dir_path: Path) -> Path:
    """Write JSON config to a temp file and return its path."""
    config_path = dir_path / "config.json"
    config_path.write_text(json.dumps(data), encoding="utf-8")
    return config_path


# --- Tests ---


class TestFromJsonToComplete:
    """JSON config file → LLMRotator.from_json() → complete() → response."""

    async def test_from_json_to_complete(self, tmp_path: Path):
        config_data = {
            "providers": [
                {
                    "name": "test_provider",
                    "client_type": "openai",
                    "priority": 1,
                    "models": ["gpt-4o"],
                    "keys": [{"token": "sk-test", "alias": "key1"}],
                }
            ]
        }
        config_path = _write_json_config(config_data, tmp_path)
        client = FakeLLMClient()
        client.enqueue(_response("from json config"))

        rotator = LLMRotator.from_json(config_path, clients={"openai": client})
        result = await rotator.complete(
            messages=[{"role": "user", "content": "hi"}]
        )

        assert result.content == "from json config"


class TestAnthropicInRotation:
    """Anthropic as fallback provider in the rotation chain."""

    async def test_anthropic_fallback(self):
        config = RotatorConfig(
            providers=[
                {
                    "name": "openai_primary",
                    "client_type": "openai",
                    "priority": 1,
                    "models": ["gpt-4o"],
                    "keys": [{"token": "sk-1", "alias": "key1"}],
                    "server_error_retry": {"max_attempts": 0, "delay_seconds": 0},
                },
                {
                    "name": "anthropic_fallback",
                    "client_type": "anthropic",
                    "priority": 2,
                    "models": ["claude-sonnet-4-20250514"],
                    "keys": [{"token": "sk-ant-1", "alias": "ant_key1"}],
                },
            ]
        )
        openai_client = FakeLLMClient()
        anthropic_client = FakeLLMClient()

        # OpenAI fails with 500
        openai_client.enqueue(ServerError("OpenAI down", status_code=500))
        # Anthropic succeeds
        anthropic_client.enqueue(_response("from anthropic", model="claude-sonnet-4-20250514"))

        rotator = LLMRotator(
            config,
            clients={"openai": openai_client, "anthropic": anthropic_client},
        )
        result = await rotator.complete(
            messages=[{"role": "user", "content": "hi"}]
        )

        assert result.content == "from anthropic"
        assert result.model == "claude-sonnet-4-20250514"


class TestToolCallingWithFallback:
    """Tool call → 429 → fallback on another provider → tool call response."""

    async def test_tool_call_fallback(self):
        config = RotatorConfig(
            providers=[
                {
                    "name": "provider_a",
                    "client_type": "openai",
                    "priority": 1,
                    "models": ["gpt-4o"],
                    "keys": [{"token": "sk-1", "alias": "key1"}],
                    "server_error_retry": {"max_attempts": 0, "delay_seconds": 0},
                },
                {
                    "name": "provider_b",
                    "client_type": "openai",
                    "priority": 2,
                    "models": ["gpt-4o"],
                    "keys": [{"token": "sk-2", "alias": "key2"}],
                },
            ]
        )
        client = FakeLLMClient()
        # First provider returns 500
        client.enqueue(ServerError("Provider A error", status_code=500))
        # Second provider returns tool call
        client.enqueue(
            _response(
                content=None,
                tool_calls=[
                    ToolCall(
                        id="call_abc",
                        name="get_weather",
                        arguments='{"city": "Moscow"}',
                    )
                ],
            )
        )

        tools = [
            {
                "type": "function",
                "function": {
                    "name": "get_weather",
                    "description": "Get weather",
                    "parameters": {
                        "type": "object",
                        "properties": {"city": {"type": "string"}},
                        "required": ["city"],
                    },
                },
            }
        ]

        rotator = LLMRotator(config, clients={"openai": client})
        result = await rotator.complete(
            messages=[{"role": "user", "content": "Weather in Moscow?"}],
            tools=tools,
        )

        assert result.tool_calls is not None
        assert len(result.tool_calls) == 1
        assert result.tool_calls[0].name == "get_weather"
        assert result.tool_calls[0].arguments == '{"city": "Moscow"}'

        # Verify tools were forwarded in both calls
        assert client.calls[0].get("tools") == tools
        assert client.calls[1].get("tools") == tools


class TestStructuredOutputCrossProvider:
    """Pydantic schema → different providers → same result format."""

    async def test_response_format_forwarded(self):
        config = RotatorConfig(
            providers=[
                {
                    "name": "openai",
                    "client_type": "openai",
                    "priority": 1,
                    "models": ["gpt-4o"],
                    "keys": [{"token": "sk-1", "alias": "key1"}],
                    "server_error_retry": {"max_attempts": 0, "delay_seconds": 0},
                },
                {
                    "name": "gemini",
                    "client_type": "gemini",
                    "priority": 2,
                    "models": ["gemini-flash"],
                    "keys": [{"token": "gk-1", "alias": "gkey1"}],
                },
            ]
        )
        openai_client = FakeLLMClient()
        gemini_client = FakeLLMClient()

        response_format = {"type": "json_object"}

        # OpenAI fails
        openai_client.enqueue(ServerError("OpenAI error", status_code=500))
        # Gemini succeeds
        gemini_client.enqueue(
            _response(content='{"name": "Alice", "age": 30}', model="gemini-flash")
        )

        rotator = LLMRotator(
            config,
            clients={"openai": openai_client, "gemini": gemini_client},
        )
        result = await rotator.complete(
            messages=[{"role": "user", "content": "Give me a person JSON"}],
            response_format=response_format,
        )

        assert result.content == '{"name": "Alice", "age": 30}'
        # Verify response_format forwarded to both clients
        assert openai_client.calls[0].get("response_format") == response_format
        assert gemini_client.calls[0].get("response_format") == response_format


class TestQuotaWarningInRotation:
    """Quota warning callback fires at 80% during rotation."""

    async def test_quota_warning_fires(self):
        config = RotatorConfig(
            providers=[
                {
                    "name": "provider_a",
                    "client_type": "openai",
                    "priority": 1,
                    "model_groups": [
                        {
                            "name": "flagship",
                            "tier": 1,
                            "models": ["gpt-4o"],
                            "token_quota": {"limit": 20, "reset": "daily_utc"},
                        },
                    ],
                    "keys": [{"token": "sk-1", "alias": "key1"}],
                }
            ]
        )
        client = FakeLLMClient()
        # Each response uses 15 tokens total → first call: 15/20 = 75% (no warning)
        client.enqueue(_response("first", model="gpt-4o"))
        # Second call: 30/20 → would be 150%, but pre-check blocks at usage=15
        # Actually after first call usage=15, second call pre_check: 15 < 20 OK
        # After second call: usage = 30, 30/20 = 150% > 80% → warning fires
        client.enqueue(_response("second", model="gpt-4o"))

        warnings_received: list[QuotaWarning] = []

        async def on_warning(w: QuotaWarning) -> None:
            warnings_received.append(w)

        rotator = LLMRotator(
            config,
            clients={"openai": client},
            on_quota_warning=on_warning,
            warning_threshold=0.8,
        )

        # First call: 15/20 = 75% → no warning
        await rotator.complete(messages=[{"role": "user", "content": "1"}])
        assert len(warnings_received) == 0

        # Second call: usage after = 30/20 = 150% → warning!
        await rotator.complete(messages=[{"role": "user", "content": "2"}])
        assert len(warnings_received) == 1
        assert warnings_received[0].scope == "provider_a/flagship"
        assert warnings_received[0].percentage >= 0.8


class TestRedisBackendFullRotation:
    """Full rotation flow using RedisBackend (via fakeredis)."""

    async def test_redis_rotation(self):
        fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
        backend = RedisBackend(redis=fake_redis)

        config = RotatorConfig(
            providers=[
                {
                    "name": "provider_a",
                    "client_type": "openai",
                    "priority": 1,
                    "model_groups": [
                        {
                            "name": "flagship",
                            "tier": 1,
                            "models": ["gpt-4o"],
                            "token_quota": {"limit": 20, "reset": "daily_utc"},
                        },
                        {
                            "name": "mini",
                            "tier": 3,
                            "models": ["gpt-4o-mini"],
                        },
                    ],
                    "keys": [{"token": "sk-1", "alias": "key1"}],
                }
            ]
        )
        client = FakeLLMClient()
        client.enqueue(_response("flagship ok", model="gpt-4o"))
        client.enqueue(_response("flagship ok 2", model="gpt-4o"))
        client.enqueue(_response("mini ok", model="gpt-4o-mini"))

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

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

        # Second call: still under quota (15 < 20)
        r2 = await rotator.complete(messages=[{"role": "user", "content": "2"}])
        assert r2.model == "gpt-4o"

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

        # Verify quota was actually stored in Redis
        usage = await fake_redis.get("llm_rotator:quota:provider_a/flagship")
        assert int(usage) == 30

        await fake_redis.aclose()


class TestLangChainChainWithRotator:
    """prompt | RotatorChatModel | parser with fallback."""

    async def test_chain_with_fallback(self):
        config = RotatorConfig(
            providers=[
                {
                    "name": "primary",
                    "client_type": "openai",
                    "priority": 1,
                    "models": ["gpt-4o"],
                    "keys": [{"token": "sk-1", "alias": "key1"}],
                },
                {
                    "name": "fallback",
                    "client_type": "openai",
                    "priority": 2,
                    "models": ["gpt-4o"],
                    "keys": [{"token": "sk-2", "alias": "key2"}],
                },
            ]
        )
        client = FakeLLMClient()
        # Primary fails
        client.enqueue(ServerError("primary down", status_code=500))
        # Fallback succeeds
        client.enqueue(_response("Paris"))

        rotator = LLMRotator(config, clients={"openai": client})
        model = RotatorChatModel(rotator=rotator)

        prompt = ChatPromptTemplate.from_messages(
            [
                ("system", "Answer in one word."),
                ("user", "{question}"),
            ]
        )
        chain = prompt | model | StrOutputParser()
        result = await chain.ainvoke({"question": "Capital of France?"})

        assert result == "Paris"


class TestStructuredLoggingOutput:
    """Full routing chain appears in structured logs."""

    async def test_logging_chain(self):
        config = RotatorConfig(
            providers=[
                {
                    "name": "OpenAI",
                    "client_type": "openai",
                    "priority": 1,
                    "model_groups": [
                        {"name": "flagship", "tier": 1, "models": ["gpt-4o"]},
                    ],
                    "keys": [
                        {"token": "sk-1", "alias": "main_key"},
                        {"token": "sk-2", "alias": "backup_key"},
                    ],
                    "server_error_retry": {"max_attempts": 0, "delay_seconds": 0},
                },
                {
                    "name": "Gemini",
                    "client_type": "gemini",
                    "priority": 2,
                    "models": ["gemini-flash"],
                    "keys": [{"token": "gk-1", "alias": "google_key"}],
                },
            ]
        )
        client = FakeLLMClient()
        # main_key → 500
        client.enqueue(ServerError("error", status_code=500))
        # backup_key → 500
        client.enqueue(ServerError("error", status_code=500))
        # gemini → success
        gemini_client = FakeLLMClient()
        gemini_client.enqueue(_response("gemini ok", model="gemini-flash"))

        test_logger = logging.getLogger("test_routing")
        log_records: list[logging.LogRecord] = []

        class RecordHandler(logging.Handler):
            def emit(self, record: logging.LogRecord) -> None:
                log_records.append(record)

        handler = RecordHandler()
        test_logger.addHandler(handler)
        test_logger.setLevel(logging.DEBUG)

        rotator = LLMRotator(
            config,
            clients={"openai": client, "gemini": gemini_client},
            logger=test_logger,
        )

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

        assert result.content == "gemini ok"

        # Check that log output contains the routing chain
        assert len(log_records) >= 1
        log_message = log_records[-1].getMessage()
        assert "[req:" in log_message
        assert "500 ServerError" in log_message
        assert "200 OK" in log_message

        test_logger.removeHandler(handler)
