import logging

import litellm
import pytest
from litellm.types.utils import Choices, Message, ModelResponse, Usage

from agents.extensions.models.litellm_model import LitellmModel
from agents.model_settings import ModelSettings
from agents.models.interface import ModelTracing


@pytest.mark.allow_call_model_methods
@pytest.mark.asyncio
async def test_extra_body_is_forwarded(monkeypatch):
    """
    Forward `extra_body` via LiteLLM's dedicated kwarg.

    This ensures that provider-specific request fields stay nested under `extra_body`
    so LiteLLM can merge them into the upstream request body itself.
    """
    captured: dict[str, object] = {}

    async def fake_acompletion(model, messages=None, **kwargs):
        captured.update(kwargs)
        msg = Message(role="assistant", content="ok")
        choice = Choices(index=0, message=msg)
        return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))

    monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
    settings = ModelSettings(
        temperature=0.1, extra_body={"cached_content": "some_cache", "foo": 123}
    )
    model = LitellmModel(model="test-model")

    await model.get_response(
        system_instructions=None,
        input=[],
        model_settings=settings,
        tools=[],
        output_schema=None,
        handoffs=[],
        tracing=ModelTracing.DISABLED,
        previous_response_id=None,
    )

    assert captured["extra_body"] == {"cached_content": "some_cache", "foo": 123}
    assert "cached_content" not in captured
    assert "foo" not in captured


@pytest.mark.allow_call_model_methods
@pytest.mark.asyncio
async def test_extra_body_reasoning_effort_is_promoted(monkeypatch):
    """
    Ensure reasoning_effort from extra_body is promoted to the top-level parameter.
    """
    captured: dict[str, object] = {}

    async def fake_acompletion(model, messages=None, **kwargs):
        captured.update(kwargs)
        msg = Message(role="assistant", content="ok")
        choice = Choices(index=0, message=msg)
        return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))

    monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
    # GitHub issue context: https://github.com/openai/openai-agents-python/issues/1764.
    settings = ModelSettings(
        extra_body={"reasoning_effort": "none", "cached_content": "some_cache"}
    )
    model = LitellmModel(model="test-model")

    await model.get_response(
        system_instructions=None,
        input=[],
        model_settings=settings,
        tools=[],
        output_schema=None,
        handoffs=[],
        tracing=ModelTracing.DISABLED,
        previous_response_id=None,
    )

    assert captured["reasoning_effort"] == "none"
    assert captured["extra_body"] == {"cached_content": "some_cache"}
    assert settings.extra_body == {"reasoning_effort": "none", "cached_content": "some_cache"}


@pytest.mark.allow_call_model_methods
@pytest.mark.asyncio
async def test_reasoning_effort_prefers_model_settings(monkeypatch):
    """
    Verify explicit ModelSettings.reasoning takes precedence over extra_body entries.
    """
    from openai.types.shared import Reasoning

    captured: dict[str, object] = {}

    async def fake_acompletion(model, messages=None, **kwargs):
        captured.update(kwargs)
        msg = Message(role="assistant", content="ok")
        choice = Choices(index=0, message=msg)
        return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))

    monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
    settings = ModelSettings(
        reasoning=Reasoning(effort="low"),
        extra_body={"reasoning_effort": "high"},
    )
    model = LitellmModel(model="test-model")

    await model.get_response(
        system_instructions=None,
        input=[],
        model_settings=settings,
        tools=[],
        output_schema=None,
        handoffs=[],
        tracing=ModelTracing.DISABLED,
        previous_response_id=None,
    )

    # reasoning_effort is string when no summary is provided (backward compatible)
    assert captured["reasoning_effort"] == "low"
    assert "extra_body" not in captured
    assert settings.extra_body == {"reasoning_effort": "high"}


@pytest.mark.allow_call_model_methods
@pytest.mark.asyncio
async def test_extra_body_reasoning_effort_overrides_extra_args(monkeypatch):
    """
    Ensure extra_body reasoning_effort wins over extra_args when both are provided.
    """
    captured: dict[str, object] = {}

    async def fake_acompletion(model, messages=None, **kwargs):
        captured.update(kwargs)
        msg = Message(role="assistant", content="ok")
        choice = Choices(index=0, message=msg)
        return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))

    monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
    # GitHub issue context: https://github.com/openai/openai-agents-python/issues/1764.
    settings = ModelSettings(
        extra_body={"reasoning_effort": "none"},
        extra_args={"reasoning_effort": "low", "custom_param": "custom"},
    )
    model = LitellmModel(model="test-model")

    await model.get_response(
        system_instructions=None,
        input=[],
        model_settings=settings,
        tools=[],
        output_schema=None,
        handoffs=[],
        tracing=ModelTracing.DISABLED,
        previous_response_id=None,
    )

    assert captured["reasoning_effort"] == "none"
    assert captured["custom_param"] == "custom"
    assert "extra_body" not in captured
    assert settings.extra_args == {"reasoning_effort": "low", "custom_param": "custom"}


@pytest.mark.allow_call_model_methods
@pytest.mark.asyncio
async def test_extra_body_metadata_stays_nested(monkeypatch):
    """
    Keep extra_body metadata nested even when top-level metadata is also set.

    LiteLLM resolves top-level metadata and extra_body separately. Flattening the nested
    metadata dict loses the caller's intended request shape for OpenAI-compatible proxies.
    """
    captured: dict[str, object] = {}

    async def fake_acompletion(model, messages=None, **kwargs):
        captured.update(kwargs)
        msg = Message(role="assistant", content="ok")
        choice = Choices(index=0, message=msg)
        return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))

    monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
    settings = ModelSettings(
        metadata={"sdk": "agents"},
        extra_body={
            "metadata": {"trace_user_id": "user-123", "generation_id": "gen-456"},
            "cached_content": "some_cache",
        },
    )
    model = LitellmModel(model="test-model")

    await model.get_response(
        system_instructions=None,
        input=[],
        model_settings=settings,
        tools=[],
        output_schema=None,
        handoffs=[],
        tracing=ModelTracing.DISABLED,
        previous_response_id=None,
    )

    assert captured["metadata"] == {"sdk": "agents"}
    assert captured["extra_body"] == {
        "metadata": {"trace_user_id": "user-123", "generation_id": "gen-456"},
        "cached_content": "some_cache",
    }


@pytest.mark.allow_call_model_methods
@pytest.mark.asyncio
@pytest.mark.parametrize(
    "model_name",
    [
        "openai/gpt-5-mini",
        "anthropic/claude-sonnet-4-5",
        "gemini/gemini-2.5-pro",
    ],
)
async def test_reasoning_summary_uses_scalar_effort_and_warns(
    monkeypatch, caplog: pytest.LogCaptureFixture, model_name: str
):
    """
    Ensure reasoning.summary does not change the LiteLLM chat-completions argument shape.

    LitellmModel should continue to pass a scalar reasoning_effort value and warn that summary
    is ignored on this path, regardless of the provider encoded in the model string.
    """
    from openai.types.shared import Reasoning

    captured: dict[str, object] = {}

    async def fake_acompletion(model, messages=None, **kwargs):
        captured.update(kwargs)
        msg = Message(role="assistant", content="ok")
        choice = Choices(index=0, message=msg)
        return ModelResponse(choices=[choice], usage=Usage(0, 0, 0))

    monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
    settings = ModelSettings(
        reasoning=Reasoning(effort="medium", summary="auto"),
    )
    model = LitellmModel(model=model_name)

    with caplog.at_level(logging.WARNING, logger="openai.agents"):
        await model.get_response(
            system_instructions=None,
            input=[],
            model_settings=settings,
            tools=[],
            output_schema=None,
            handoffs=[],
            tracing=ModelTracing.DISABLED,
            previous_response_id=None,
        )

    assert captured["reasoning_effort"] == "medium"
    warning_messages = [
        record.message
        for record in caplog.records
        if "does not forward Reasoning.summary" in record.message
    ]
    assert len(warning_messages) == 1
