"""Unit tests for LangSmith LLM Gateway routing (agent/utils/gateway.py + make_model)."""

from __future__ import annotations

from typing import Any
from unittest.mock import patch

import httpx
import pytest
from fireworks import AsyncFireworks
from langchain_core.messages import AIMessage, HumanMessage
from langchain_openai import ChatOpenAI

from agent.utils import gateway, model

_GATEWAY_ENV_VARS = (
    "LANGSMITH_API_KEY",
    "LANGSMITH_API_KEY_PROD",
    "LANGSMITH_GATEWAY_API_KEY",
    "LANGSMITH_GATEWAY_ENABLED",
    "LANGSMITH_GATEWAY_BASE_URL",
    "LANGSMITH_GATEWAY_OPENAI_USE_RESPONSES",
)


@pytest.fixture(autouse=True)
def _clean_gateway_env(monkeypatch: pytest.MonkeyPatch) -> None:
    """Start each test from a known env: no key, gateway off, default base URL."""
    for name in _GATEWAY_ENV_VARS:
        monkeypatch.delenv(name, raising=False)


# --- gateway_overrides --------------------------------------------------------


def test_openai_overrides_use_responses_by_default(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    overrides = gateway.gateway_overrides("openai:gpt-5.5")
    assert overrides == {
        "base_url": "https://gateway.smith.langchain.com/openai/v1",
        "api_key": "ls-key",
        "use_responses_api": True,
    }


def test_openai_overrides_chat_completions_optout(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    monkeypatch.setenv("LANGSMITH_GATEWAY_OPENAI_USE_RESPONSES", "false")
    overrides = gateway.gateway_overrides("openai:gpt-5.5")
    assert overrides is not None
    assert overrides["use_responses_api"] is False


async def test_openai_sdk_uses_gateway_responses_path() -> None:
    requests: list[httpx.Request] = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(
            200,
            json={
                "id": "resp_test",
                "object": "response",
                "created_at": 0,
                "status": "completed",
                "model": "gpt-5.5",
                "output": [
                    {
                        "id": "msg_test",
                        "type": "message",
                        "role": "assistant",
                        "status": "completed",
                        "content": [{"type": "output_text", "text": "ok", "annotations": []}],
                    }
                ],
                "usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
            },
        )

    http_client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
    try:
        chat_model = ChatOpenAI(
            model="gpt-5.5",
            api_key="dummy",
            base_url="https://gateway.smith.langchain.com/openai/v1",
            use_responses_api=True,
            http_async_client=http_client,
            max_retries=0,
        )
        await chat_model.ainvoke([HumanMessage(content="hi")])
    finally:
        await http_client.aclose()

    assert len(requests) == 1
    assert requests[0].url.path == "/openai/v1/responses"


def test_anthropic_overrides_have_no_responses_flag(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    overrides = gateway.gateway_overrides("anthropic:claude-opus-4-8")
    assert overrides == {
        "base_url": "https://gateway.smith.langchain.com/anthropic",
        "api_key": "ls-key",
    }


def test_fireworks_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    overrides = gateway.gateway_overrides("fireworks:accounts/fireworks/models/glm-5p2")
    assert overrides is not None
    assert overrides["base_url"] == "https://gateway.smith.langchain.com/fireworks"


async def test_fireworks_sdk_uses_allowlisted_gateway_path() -> None:
    requests: list[httpx.Request] = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(
            200,
            json={
                "id": "chatcmpl-test",
                "object": "chat.completion",
                "created": 0,
                "model": "accounts/fireworks/models/glm-5p2",
                "choices": [
                    {
                        "index": 0,
                        "message": {"role": "assistant", "content": "ok"},
                        "finish_reason": "stop",
                    }
                ],
                "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
            },
        )

    http_client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
    try:
        client = AsyncFireworks(
            api_key="dummy",
            base_url="https://gateway.smith.langchain.com/fireworks",
            http_client=http_client,
            max_retries=0,
        )
        await client.chat.completions.create(
            model="accounts/fireworks/models/glm-5p2",
            messages=[{"role": "user", "content": "hi"}],
        )
    finally:
        await http_client.aclose()

    assert len(requests) == 1
    assert requests[0].url.path == "/fireworks/v1/chat/completions"


async def test_fireworks_gateway_strips_legacy_function_call() -> None:
    """The serializer must not emit ``function_call`` after sanitization.

    Reproduces the production 400 — ``Extra inputs are not permitted, field:
    'messages[N].function_call'`` — by routing an ``AIMessage`` that carries the
    legacy ``function_call`` (alongside modern ``tool_calls``) through the
    Fireworks serializer toward the gateway. Without the sanitizer middleware
    the request body contains ``function_call``; with it, only ``tool_calls``
    survives.
    """
    import json

    from fireworks import AsyncFireworks
    from langchain_fireworks.chat_models import ChatFireworks

    from agent.middleware.sanitize_fireworks_messages import _sanitize_messages

    captured_bodies: list[dict] = []

    def handler(request: httpx.Request) -> httpx.Response:
        captured_bodies.append(json.loads(request.content))
        return httpx.Response(
            200,
            json={
                "id": "chatcmpl-test",
                "object": "chat.completion",
                "created": 0,
                "model": "accounts/fireworks/models/glm-5p2",
                "choices": [
                    {
                        "index": 0,
                        "message": {"role": "assistant", "content": "ok"},
                        "finish_reason": "stop",
                    }
                ],
                "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
            },
        )

    http_client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
    try:
        chat_model = ChatFireworks(
            model="accounts/fireworks/models/glm-5p2",
            api_key="dummy",
            base_url="https://gateway.smith.langchain.com/fireworks",
            max_retries=0,
        )
        # Inject a mock-transport client so no real network call is made.
        mock_sdk = AsyncFireworks(
            api_key="dummy",
            base_url="https://gateway.smith.langchain.com/fireworks",
            http_client=http_client,
            max_retries=0,
        )
        chat_model._async_sdk_client = mock_sdk  # type: ignore[attr-defined]
        chat_model.async_client = mock_sdk.chat.completions  # type: ignore[attr-defined]

        ai_message = AIMessage(
            content="",
            tool_calls=[{"name": "read_file", "args": {"file_path": "/x"}, "id": "tc1"}],
            additional_kwargs={"function_call": {"name": "read_file", "arguments": "{}"}},
        )
        messages = [HumanMessage(content="hi"), ai_message]

        # Apply the sanitizer the same way the middleware stack does.
        _sanitize_messages(messages)

        await chat_model.ainvoke(messages)
        await mock_sdk.close()
    finally:
        await http_client.aclose()

    assert len(captured_bodies) == 1
    body = captured_bodies[0]
    for msg in body["messages"]:
        assert "function_call" not in msg, msg
    # The assistant message still carries tool_calls.
    assistant_msgs = [m for m in body["messages"] if m["role"] == "assistant"]
    assert assistant_msgs and "tool_calls" in assistant_msgs[0]


def test_google_genai_routes_to_gemini(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    overrides = gateway.gateway_overrides("google_genai:gemini-3.5-flash")
    assert overrides == {
        "base_url": "https://gateway.smith.langchain.com/gemini",
        "api_key": "ls-key",
    }


def test_unsupported_provider_passes_through(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    # Vertex authenticates with a service account, not a bearer key, so it isn't routed.
    assert gateway.gateway_overrides("google_vertexai:gemini-2.5-pro") is None


def test_missing_api_key_passes_through(monkeypatch: pytest.MonkeyPatch) -> None:
    assert gateway.gateway_overrides("openai:gpt-5.5") is None


def test_prod_key_used_as_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY_PROD", "ls-prod-key")
    overrides = gateway.gateway_overrides("anthropic:claude-opus-4-8")
    assert overrides is not None
    assert overrides["api_key"] == "ls-prod-key"


def test_prod_key_preferred_over_platform_key(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-platform-key")
    monkeypatch.setenv("LANGSMITH_API_KEY_PROD", "ls-prod-key")
    overrides = gateway.gateway_overrides("anthropic:claude-opus-4-8")
    assert overrides is not None
    assert overrides["api_key"] == "ls-prod-key"


def test_gateway_key_preferred_over_prod_key(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-platform-key")
    monkeypatch.setenv("LANGSMITH_API_KEY_PROD", "ls-prod-key")
    monkeypatch.setenv("LANGSMITH_GATEWAY_API_KEY", "ls-gateway-key")
    overrides = gateway.gateway_overrides("anthropic:claude-opus-4-8")
    assert overrides is not None
    assert overrides["api_key"] == "ls-gateway-key"


def test_base_url_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    monkeypatch.setenv("LANGSMITH_GATEWAY_BASE_URL", "https://gw.internal.example.com/")
    overrides = gateway.gateway_overrides("anthropic:claude-opus-4-8")
    assert overrides is not None
    # Trailing slash is stripped, then the provider path is appended.
    assert overrides["base_url"] == "https://gw.internal.example.com/anthropic"


# --- resolve_gateway_enabled --------------------------------------------------


@pytest.mark.parametrize(
    ("team_value", "env_enabled", "expected"),
    [
        (True, False, True),  # team True wins over env off
        (False, True, False),  # team False wins over env on
        (None, True, True),  # unset inherits env on
        (None, False, False),  # unset inherits env off
    ],
)
def test_resolve_gateway_enabled_precedence(
    monkeypatch: pytest.MonkeyPatch,
    team_value: bool | None,
    env_enabled: bool,
    expected: bool,
) -> None:
    if env_enabled:
        monkeypatch.setenv("LANGSMITH_GATEWAY_ENABLED", "true")
    assert gateway.resolve_gateway_enabled(team_value) is expected


# --- make_model integration ---------------------------------------------------


def _capture_init_chat_model() -> tuple[dict[str, Any], Any]:
    """Patch init_chat_model to record the kwargs make_model builds."""
    captured: dict[str, Any] = {}

    def _fake(model: str, **kwargs: Any) -> str:
        captured["model"] = model
        captured.update(kwargs)
        return "MODEL"

    return captured, _fake


def test_make_model_direct_openai_uses_responses_websocket() -> None:
    captured, fake = _capture_init_chat_model()
    with patch.object(model, "init_chat_model", fake):
        model.make_model("openai:gpt-5.5", use_gateway=False)
    assert captured["base_url"] == model.OPENAI_RESPONSES_WS_BASE_URL
    assert captured["use_responses_api"] is True
    assert captured["store"] is False
    assert captured["include"] == ["reasoning.encrypted_content"]


def test_make_model_gateway_openai_replaces_websocket(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    captured, fake = _capture_init_chat_model()
    with patch.object(model, "init_chat_model", fake):
        model.make_model("openai:gpt-5.5", use_gateway=True)
    assert captured["base_url"] == "https://gateway.smith.langchain.com/openai/v1"
    assert captured["use_responses_api"] is True
    assert captured["store"] is False
    assert captured["include"] == ["reasoning.encrypted_content"]
    assert captured["api_key"] == "ls-key"


def test_make_model_gateway_openai_chat_completions_optout_converts_reasoning(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    monkeypatch.setenv("LANGSMITH_GATEWAY_OPENAI_USE_RESPONSES", "false")
    captured, fake = _capture_init_chat_model()
    with patch.object(model, "init_chat_model", fake):
        model.make_model(
            "openai:gpt-5.5",
            use_gateway=True,
            reasoning={"effort": "high", "summary": "auto"},
        )
    assert captured["use_responses_api"] is False
    assert captured["reasoning_effort"] == "high"
    assert "reasoning" not in captured
    assert "include" not in captured
    assert "store" not in captured


def test_make_model_gateway_openai_preserves_reasoning_none(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    captured, fake = _capture_init_chat_model()
    with patch.object(model, "init_chat_model", fake):
        model.make_model(
            "openai:gpt-5.5",
            use_gateway=True,
            reasoning={"effort": "none"},
        )
    assert captured["use_responses_api"] is True
    assert captured["store"] is False
    assert captured["include"] == ["reasoning.encrypted_content"]
    assert captured["reasoning"] == {"effort": "none"}
    assert "reasoning_effort" not in captured


def test_make_model_gateway_openai_responses_keeps_reasoning(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    reasoning = {"effort": "high", "summary": "auto"}
    captured, fake = _capture_init_chat_model()
    with patch.object(model, "init_chat_model", fake):
        model.make_model("openai:gpt-5.5", use_gateway=True, reasoning=reasoning)
    assert captured["use_responses_api"] is True
    assert captured["store"] is False
    assert captured["include"] == ["reasoning.encrypted_content"]
    assert captured["reasoning"] == reasoning
    assert "reasoning_effort" not in captured


def test_make_model_gateway_follows_env_default(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    monkeypatch.setenv("LANGSMITH_GATEWAY_ENABLED", "true")
    captured, fake = _capture_init_chat_model()
    with patch.object(model, "init_chat_model", fake):
        model.make_model("anthropic:claude-opus-4-8")  # use_gateway=None -> env default
    assert captured["base_url"] == "https://gateway.smith.langchain.com/anthropic"
    assert captured["api_key"] == "ls-key"


def test_make_model_gateway_google_genai(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LANGSMITH_API_KEY", "ls-key")
    captured, fake = _capture_init_chat_model()
    with patch.object(model, "init_chat_model", fake):
        model.make_model("google_genai:gemini-3.5-flash", use_gateway=True)
    assert captured["base_url"] == "https://gateway.smith.langchain.com/gemini"
    assert captured["api_key"] == "ls-key"


def test_make_model_gateway_without_key_falls_back_direct(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    captured, fake = _capture_init_chat_model()
    with patch.object(model, "init_chat_model", fake):
        model.make_model("openai:gpt-5.5", use_gateway=True)  # no LangSmith key
    # No key -> overrides skipped -> the direct-provider websocket base stands.
    assert captured["base_url"] == model.OPENAI_RESPONSES_WS_BASE_URL
    assert captured["use_responses_api"] is True
    assert captured["store"] is False
    assert captured["include"] == ["reasoning.encrypted_content"]
    assert "api_key" not in captured
