"""Provider parity tests: verify that AIAgent builds correct API kwargs
and handles responses properly for all supported providers.

Ensures changes to one provider path don't silently break another.
"""

import base64
import json
import sys
import types
from types import SimpleNamespace
from unittest.mock import patch

import pytest
from agent.codex_responses_adapter import _chat_content_to_responses_parts, _chat_messages_to_responses_input, _normalize_codex_response, _preflight_codex_input_items

sys.modules.setdefault("fire", types.SimpleNamespace(Fire=lambda *a, **k: None))
sys.modules.setdefault("firecrawl", types.SimpleNamespace(Firecrawl=object))
sys.modules.setdefault("fal_client", types.SimpleNamespace())

from run_agent import AIAgent


# ── Helpers ──────────────────────────────────────────────────────────────────

def _tool_defs(*names):
    return [
        {
            "type": "function",
            "function": {
                "name": n,
                "description": f"{n} tool",
                "parameters": {"type": "object", "properties": {}},
            },
        }
        for n in names
    ]


def _fake_invoke_jwt() -> str:
    def _part(payload):
        raw = json.dumps(payload, separators=(",", ":")).encode("utf-8")
        return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")

    return (
        f"{_part({'alg': 'none', 'typ': 'JWT'})}."
        f"{_part({'scope': 'inference:invoke', 'exp': 4102444800})}.sig"
    )


class _FakeOpenAI:
    def __init__(self, **kw):
        self.api_key = kw.get("api_key", "test")
        self.base_url = kw.get("base_url", "http://test")
    def close(self):
        pass


@pytest.fixture(autouse=True)
def _reset_auxiliary_provider_state():
    from agent.auxiliary_client import _reset_aux_unhealthy_cache

    _reset_aux_unhealthy_cache()
    yield
    _reset_aux_unhealthy_cache()


def _make_agent(monkeypatch, provider, api_mode="chat_completions", base_url="https://openrouter.ai/api/v1", model=None):
    monkeypatch.setattr("run_agent.get_tool_definitions", lambda **kw: _tool_defs("web_search", "terminal"))
    monkeypatch.setattr("run_agent.check_toolset_requirements", lambda: {})
    monkeypatch.setattr("run_agent.OpenAI", _FakeOpenAI)
    kwargs = dict(
        api_key="test-key",
        base_url=base_url,
        provider=provider,
        api_mode=api_mode,
        max_iterations=4,
        quiet_mode=True,
        skip_context_files=True,
        skip_memory=True,
    )
    if model:
        kwargs["model"] = model
    elif provider == "nous":
        kwargs["model"] = "gpt-5"
    base_url="https://openrouter.ai/api/v1",
    api_key="test-key",
    base_url="https://openrouter.ai/api/v1",
    return AIAgent(**kwargs)


# ── _build_api_kwargs tests ─────────────────────────────────────────────────

class TestBuildApiKwargsOpenRouter:
    def test_uses_chat_completions_format(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openrouter")
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert "messages" in kwargs
        assert "model" in kwargs
        assert kwargs["messages"][-1]["content"] == "hi"

    def test_includes_reasoning_in_extra_body(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openrouter")
        agent.model = "anthropic/claude-sonnet-4-20250514"
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        extra = kwargs.get("extra_body", {})
        assert "reasoning" in extra
        assert extra["reasoning"]["enabled"] is True


    def test_no_responses_api_fields(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openrouter")
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert "input" not in kwargs
        assert "instructions" not in kwargs
        assert "store" not in kwargs

    def test_strips_codex_only_tool_call_fields_from_chat_messages(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openrouter")
        messages = [
            {"role": "user", "content": "hi"},
            {
                "role": "assistant",
                "content": "Checking now.",
                "codex_reasoning_items": [
                    {"type": "reasoning", "id": "rs_1", "encrypted_content": "blob"},
                ],
                "tool_calls": [
                    {
                        "id": "call_123",
                        "call_id": "call_123",
                        "response_item_id": "fc_123",
                        "type": "function",
                        "function": {"name": "terminal", "arguments": "{\"command\":\"pwd\"}"},
                        "extra_content": {"thought_signature": "opaque"},
                    }
                ],
            },
            {"role": "tool", "tool_call_id": "call_123", "content": "/tmp"},
        ]

        kwargs = agent._build_api_kwargs(messages)

        assistant_msg = kwargs["messages"][1]
        tool_call = assistant_msg["tool_calls"][0]

        assert "codex_reasoning_items" not in assistant_msg
        assert tool_call["id"] == "call_123"
        assert tool_call["function"]["name"] == "terminal"
        # extra_content (Gemini thought_signature) is stripped for non-Gemini
        # targets — strict providers like Fireworks 400 on it. The agent here
        # is not a Gemini model, so it must be dropped.
        assert "extra_content" not in tool_call
        assert "call_id" not in tool_call
        assert "response_item_id" not in tool_call

        # Original stored history must remain unchanged (only the outgoing copy
        # is sanitized) — Codex/Responses replay relies on these fields.
        assert messages[1]["tool_calls"][0]["call_id"] == "call_123"
        assert messages[1]["tool_calls"][0]["response_item_id"] == "fc_123"
        assert "codex_reasoning_items" in messages[1]
        assert messages[1]["tool_calls"][0]["extra_content"] == {"thought_signature": "opaque"}

    def test_keeps_extra_content_for_gemini_target(self, monkeypatch):
        """Gemini-family targets must keep extra_content (thought_signature) —
        Gemini 3 thinking models 400 without it replayed on the next turn.
        """
        agent = _make_agent(monkeypatch, "openrouter", model="google/gemini-3-pro-preview")
        messages = [
            {"role": "user", "content": "hi"},
            {
                "role": "assistant",
                "content": "Checking now.",
                "tool_calls": [
                    {
                        "id": "call_123",
                        "call_id": "call_123",
                        "response_item_id": "fc_123",
                        "type": "function",
                        "function": {"name": "terminal", "arguments": "{\"command\":\"pwd\"}"},
                        "extra_content": {"google": {"thought_signature": "opaque"}},
                    }
                ],
            },
            {"role": "tool", "tool_call_id": "call_123", "content": "/tmp"},
        ]

        kwargs = agent._build_api_kwargs(messages)
        tool_call = kwargs["messages"][1]["tool_calls"][0]
        assert tool_call["extra_content"] == {"google": {"thought_signature": "opaque"}}
        # call_id/response_item_id still stripped regardless of model
        assert "call_id" not in tool_call
        assert "response_item_id" not in tool_call

        # Original stored history must remain unchanged for Responses replay mode.
        assert messages[1]["tool_calls"][0]["call_id"] == "call_123"
        assert messages[1]["tool_calls"][0]["response_item_id"] == "fc_123"
        assert messages[1]["tool_calls"][0]["extra_content"] == {
            "google": {"thought_signature": "opaque"}
        }

    def test_gemini_native_passes_base_url_for_top_level_thinking_config(self, monkeypatch):
        agent = _make_agent(
            monkeypatch,
            "gemini",
            base_url="https://generativelanguage.googleapis.com/v1beta",
            model="gemini-3-flash-preview",
        )
        agent.reasoning_config = {"enabled": True, "effort": "high"}
        kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}])
        assert kwargs["extra_body"]["thinking_config"] == {
            "includeThoughts": True,
            "thinkingLevel": "high",
        }
        assert "extra_body" not in kwargs["extra_body"]


    def test_should_sanitize_tool_calls_codex_vs_chat(self, monkeypatch):
        """Codex API should NOT sanitize, all other APIs should sanitize."""
        # Codex mode should NOT need sanitization
        codex_agent = _make_agent(monkeypatch, "openrouter")
        codex_agent.api_mode = "codex_responses"
        assert codex_agent._should_sanitize_tool_calls() is False

        # Chat completions mode should need sanitization
        chat_agent = _make_agent(monkeypatch, "openrouter")
        chat_agent.api_mode = "chat_completions"
        assert chat_agent._should_sanitize_tool_calls() is True

        # Anthropic mode should need sanitization
        anthropic_agent = _make_agent(monkeypatch, "openrouter")
        anthropic_agent.api_mode = "anthropic_messages"
        assert anthropic_agent._should_sanitize_tool_calls() is True

    def _api_msg_with_extra_content(self):
        return {
            "role": "assistant",
            "content": None,
            "tool_calls": [
                {"id": "call_1", "call_id": "call_1", "type": "function",
                 "extra_content": {"google": {"thought_signature": "SIG_123"}},
                 "function": {"name": "t", "arguments": "{}"}},
            ],
        }

    def test_sanitize_tool_calls_strips_extra_content_for_strict_model(self, monkeypatch):
        """Strict providers reject extra_content; strip it for non-Gemini models."""
        agent = _make_agent(monkeypatch, "openrouter")
        api_msg = self._api_msg_with_extra_content()
        result = agent._sanitize_tool_calls_for_strict_api(
            api_msg, model="accounts/fireworks/models/llama-v3p1-70b"
        )
        assert "extra_content" not in result["tool_calls"][0]
        assert "call_id" not in result["tool_calls"][0]




class TestDeveloperRoleSwap:
    """GPT-5 and Codex models should get 'developer' instead of 'system' role."""

    @pytest.mark.parametrize("model", [
        "openai/gpt-5",
        "openai/gpt-5-turbo",
        "openai/gpt-5.4",
        "gpt-5-mini",
        "openai/codex-mini",
        "codex-mini-latest",
        "openai/codex-pro",
    ])
    def test_gpt5_codex_get_developer_role(self, monkeypatch, model):
        agent = _make_agent(monkeypatch, "openrouter")
        agent.model = model
        messages = [
            {"role": "system", "content": "You are helpful."},
            {"role": "user", "content": "hi"},
        ]
        kwargs = agent._build_api_kwargs(messages)
        assert kwargs["messages"][0]["role"] == "developer"
        assert kwargs["messages"][0]["content"] == "You are helpful."
        assert kwargs["messages"][1]["role"] == "user"






class TestBuildApiKwargsChatCompletionsServiceTier:
    """service_tier via request_overrides works on the chat_completions path."""

    def test_includes_service_tier_via_request_overrides(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openrouter")
        agent.model = "gpt-4.1"
        agent.request_overrides = {"service_tier": "priority"}
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert kwargs["service_tier"] == "priority"




class TestBuildApiKwargsKimiNoTemperatureOverride:
    def test_kimi_for_coding_omits_temperature(self, monkeypatch):
        """Temperature should NOT be set client-side for Kimi models.

        The Kimi gateway selects the correct temperature server-side.
        """
        agent = _make_agent(
            monkeypatch,
            "kimi-coding",
            base_url="https://api.kimi.com/coding/v1",
            model="kimi-for-coding",
        )
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert "temperature" not in kwargs


class TestBuildApiKwargsAIGateway:
    def test_uses_chat_completions_format(self, monkeypatch):
        agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1", model="gpt-4o")
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert "messages" in kwargs
        assert "model" in kwargs
        assert kwargs["messages"][-1]["content"] == "hi"

    def test_no_responses_api_fields(self, monkeypatch):
        agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1", model="gpt-4o")
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert "input" not in kwargs
        assert "instructions" not in kwargs
        assert "store" not in kwargs

    def test_includes_reasoning_in_extra_body(self, monkeypatch):
        agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1", model="gpt-4o")
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        extra = kwargs.get("extra_body", {})
        assert "reasoning" in extra
        assert extra["reasoning"]["enabled"] is True

    def test_includes_tools(self, monkeypatch):
        agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1", model="gpt-4o")
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert "tools" in kwargs
        tool_names = [t["function"]["name"] for t in kwargs["tools"]]
        assert "web_search" in tool_names


class TestBuildApiKwargsNousPortal:
    def test_includes_nous_product_tags(self, monkeypatch):
        from agent.portal_tags import nous_portal_tags
        agent = _make_agent(
            monkeypatch,
            "nous",
            base_url="https://inference-api.nousresearch.com/v1",
            model="gpt-5",
        )
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        extra = kwargs.get("extra_body", {})
        assert extra.get("tags") == nous_portal_tags(session_id=agent.session_id)

    def test_uses_chat_completions_format(self, monkeypatch):
        agent = _make_agent(
            monkeypatch,
            "nous",
            base_url="https://inference-api.nousresearch.com/v1",
            model="gpt-5",
        )
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert "messages" in kwargs
        assert "input" not in kwargs


class TestBuildApiKwargsCustomEndpoint:
    def test_uses_chat_completions_format(self, monkeypatch):
        agent = _make_agent(monkeypatch, "custom", base_url="http://localhost:1234/v1")
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert "messages" in kwargs
        assert "input" not in kwargs


    def test_fireworks_tool_call_payload_strips_codex_only_fields(self, monkeypatch):
        agent = _make_agent(
            monkeypatch,
            "custom",
            base_url="https://api.fireworks.ai/inference/v1",
        )
        messages = [
            {"role": "user", "content": "hi"},
            {
                "role": "assistant",
                "content": "Checking now.",
                "codex_reasoning_items": [
                    {"type": "reasoning", "id": "rs_1", "encrypted_content": "blob"},
                ],
                "tool_calls": [
                    {
                        "id": "call_fw_123",
                        "call_id": "call_fw_123",
                        "response_item_id": "fc_fw_123",
                        "type": "function",
                        "function": {
                            "name": "terminal",
                            "arguments": "{\"command\":\"pwd\"}",
                        },
                    }
                ],
            },
            {"role": "tool", "tool_call_id": "call_fw_123", "content": "/tmp"},
        ]

        kwargs = agent._build_api_kwargs(messages)

        assert kwargs["tools"][0]["function"]["name"] == "web_search"
        assert "input" not in kwargs
        assert kwargs.get("extra_body", {}) == {}

        assistant_msg = kwargs["messages"][1]
        tool_call = assistant_msg["tool_calls"][0]

        assert "codex_reasoning_items" not in assistant_msg
        assert tool_call["id"] == "call_fw_123"
        assert tool_call["type"] == "function"
        assert tool_call["function"]["name"] == "terminal"
        assert "call_id" not in tool_call
        assert "response_item_id" not in tool_call


class TestBuildApiKwargsCodex:
    def test_uses_responses_api_format(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert "input" in kwargs
        assert "instructions" in kwargs
        assert "messages" not in kwargs
        assert kwargs["store"] is False


    def test_includes_service_tier_via_request_overrides(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        agent.model = "gpt-5.4"
        agent.service_tier = "priority"
        agent.request_overrides = {"service_tier": "priority"}
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        assert kwargs["service_tier"] == "priority"



    def test_tools_converted_to_responses_format(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        messages = [{"role": "user", "content": "hi"}]
        kwargs = agent._build_api_kwargs(messages)
        tools = kwargs.get("tools", [])
        assert len(tools) > 0
        # Responses format has "name" at top level, not nested under "function"
        assert "name" in tools[0]
        assert "function" not in tools[0]


# ── Message conversion tests ────────────────────────────────────────────────

class TestChatMessagesToResponsesInput:
    """Verify _chat_messages_to_responses_input for Codex mode."""

    def test_user_message_passes_through(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        messages = [{"role": "user", "content": "hello"}]
        items = _chat_messages_to_responses_input(messages)
        assert items == [{"role": "user", "content": "hello"}]

    def test_system_messages_filtered(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        messages = [
            {"role": "system", "content": "be helpful"},
            {"role": "user", "content": "hello"},
        ]
        items = _chat_messages_to_responses_input(messages)
        assert len(items) == 1
        assert items[0]["role"] == "user"

    def test_assistant_tool_calls_become_function_call_items(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        messages = [{
            "role": "assistant",
            "content": "",
            "tool_calls": [{
                "id": "call_abc",
                "call_id": "call_abc",
                "function": {"name": "web_search", "arguments": '{"query": "test"}'},
            }],
        }]
        items = _chat_messages_to_responses_input(messages)
        fc_items = [i for i in items if i.get("type") == "function_call"]
        assert len(fc_items) == 1
        assert fc_items[0]["name"] == "web_search"
        assert fc_items[0]["call_id"] == "call_abc"






    def test_preflight_preserves_assistant_output_text(self, monkeypatch):
        """_preflight_codex_input_items must preserve output_text for assistant."""
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        raw_input = [
            {"role": "user", "content": [{"type": "input_text", "text": "hi"}]},
            {"role": "assistant", "content": [{"type": "output_text", "text": "hello"}]},
        ]
        normalized = _preflight_codex_input_items(raw_input)
        user_content = normalized[0]["content"]
        asst_content = normalized[1]["content"]
        assert user_content[0]["type"] == "input_text"
        assert asst_content[0]["type"] == "output_text"

    def test_full_round_trip_with_list_content(self, monkeypatch):
        """End-to-end: user + assistant with list content through both stages."""
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        messages = [
            {"role": "user", "content": [{"type": "text", "text": "hello"}]},
            {"role": "assistant", "content": [{"type": "text", "text": "hi there"}]},
            {"role": "user", "content": [{"type": "text", "text": "continue"}]},
        ]
        items = _chat_messages_to_responses_input(messages)
        normalized = _preflight_codex_input_items(items)

        # User items use input_text
        assert normalized[0]["content"][0]["type"] == "input_text"
        assert normalized[2]["content"][0]["type"] == "input_text"
        # Assistant item uses output_text
        assert normalized[1]["content"][0]["type"] == "output_text"


class TestChatContentToResponsesParts:
    """Unit tests for _chat_content_to_responses_parts role parameter (#15687)."""

    def test_default_role_emits_input_text(self):
        """Default (user) role emits input_text."""
        result = _chat_content_to_responses_parts([{"type": "text", "text": "hello"}])
        assert result[0]["type"] == "input_text"




    def test_assistant_role_with_mixed_input_output_text_types(self):
        """Parts already marked input_text or output_text get normalized to role's type."""
        parts = [
            {"type": "input_text", "text": "a"},
            {"type": "output_text", "text": "b"},
            {"type": "text", "text": "c"},
        ]
        result = _chat_content_to_responses_parts(parts, role="assistant")
        # All text parts should become output_text regardless of original type
        assert all(p["type"] == "output_text" for p in result)
        assert [p["text"] for p in result] == ["a", "b", "c"]


# ── Response normalization tests ─────────────────────────────────────────────

class TestNormalizeCodexResponse:
    """Verify _normalize_codex_response extracts all fields correctly."""

    def _make_codex_agent(self, monkeypatch):
        return _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                           base_url="https://chatgpt.com/backend-api/codex")

    def test_text_response(self, monkeypatch):
        agent = self._make_codex_agent(monkeypatch)
        response = SimpleNamespace(
            output=[
                SimpleNamespace(type="message", status="completed",
                    content=[SimpleNamespace(type="output_text", text="Hello!")],
                    phase="final_answer"),
            ],
            status="completed",
        )
        msg, reason = _normalize_codex_response(response)
        assert msg.content == "Hello!"
        assert reason == "stop"


    def test_encrypted_content_captured(self, monkeypatch):
        agent = self._make_codex_agent(monkeypatch)
        response = SimpleNamespace(
            output=[
                SimpleNamespace(type="reasoning",
                    encrypted_content="gAAAA_secret_blob_123",
                    summary=[SimpleNamespace(type="summary_text", text="Thinking")],
                    id="rs_456", status=None),
                SimpleNamespace(type="message", status="completed",
                    content=[SimpleNamespace(type="output_text", text="done")],
                    phase="final_answer"),
            ],
            status="completed",
        )
        msg, reason = _normalize_codex_response(response)
        assert msg.codex_reasoning_items is not None
        assert len(msg.codex_reasoning_items) == 1
        assert msg.codex_reasoning_items[0]["encrypted_content"] == "gAAAA_secret_blob_123"
        assert msg.codex_reasoning_items[0]["id"] == "rs_456"



    def test_message_items_captured_with_id_and_phase(self, monkeypatch):
        """Exact message items (with id/phase) must be captured for cache replay."""
        agent = self._make_codex_agent(monkeypatch)
        response = SimpleNamespace(
            output=[
                SimpleNamespace(
                    type="message", status="completed", id="msg_abc",
                    phase="commentary",
                    content=[SimpleNamespace(type="output_text", text="Thinking...")],
                ),
                SimpleNamespace(
                    type="message", status="completed", id="msg_def",
                    phase="final_answer",
                    content=[SimpleNamespace(type="output_text", text="Done!")],
                ),
            ],
            status="completed",
        )
        msg, reason = _normalize_codex_response(response)
        assert msg.codex_message_items is not None
        assert len(msg.codex_message_items) == 2
        assert msg.codex_message_items[0]["id"] == "msg_abc"
        assert msg.codex_message_items[0]["phase"] == "commentary"
        assert msg.codex_message_items[0]["content"][0]["text"] == "Thinking..."
        assert msg.codex_message_items[1]["id"] == "msg_def"
        assert msg.codex_message_items[1]["phase"] == "final_answer"
        assert msg.codex_message_items[1]["content"][0]["text"] == "Done!"



class TestChatMessagesToResponsesInputMessageItems:
    """Verify codex_message_items are replayed verbatim instead of reconstructed."""

    def test_replays_exact_message_items(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        messages = [
            {
                "role": "assistant",
                "content": "Hello world",
                "codex_message_items": [
                    {
                        "type": "message",
                        "role": "assistant",
                        "status": "completed",
                        "id": "msg_123",
                        "phase": "final_answer",
                        "content": [{"type": "output_text", "text": "Hello world"}],
                    },
                ],
            },
            {"role": "user", "content": "follow up"},
        ]
        items = _chat_messages_to_responses_input(messages)
        msg_items = [i for i in items if i.get("type") == "message"]
        assert len(msg_items) == 1
        assert msg_items[0]["id"] == "msg_123"
        assert msg_items[0]["phase"] == "final_answer"
        assert msg_items[0]["content"][0]["text"] == "Hello world"

    def test_fallback_to_plain_when_no_message_items(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        messages = [{"role": "assistant", "content": "Hello world"}]
        items = _chat_messages_to_responses_input(messages)
        assert items == [{"role": "assistant", "content": "Hello world"}]



# ── Chat completions response handling (OpenRouter/Nous) ─────────────────────

class TestBuildAssistantMessage:
    """Verify _build_assistant_message works for all provider response formats."""

    def test_openrouter_reasoning_fields(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openrouter")
        msg = SimpleNamespace(
            content="answer",
            tool_calls=None,
            reasoning="I thought about it",
            reasoning_content=None,
            reasoning_details=None,
        )
        result = agent._build_assistant_message(msg, "stop")
        assert result["content"] == "answer"
        assert result["reasoning"] == "I thought about it"
        assert "codex_reasoning_items" not in result

    def test_openrouter_reasoning_details_preserved_unmodified(self, monkeypatch):
        """reasoning_details must be passed back exactly as received for
        multi-turn continuity (OpenRouter, Anthropic, OpenAI all need this)."""
        agent = _make_agent(monkeypatch, "openrouter")
        original_detail = {
            "type": "thinking",
            "thinking": "deep thoughts here",
            "signature": "sig123_opaque_blob",
            "encrypted_content": "some_provider_blob",
            "extra_field": "should_not_be_dropped",
        }
        msg = SimpleNamespace(
            content="answer",
            tool_calls=None,
            reasoning=None,
            reasoning_content=None,
            reasoning_details=[original_detail],
        )
        result = agent._build_assistant_message(msg, "stop")
        stored = result["reasoning_details"][0]
        # ALL fields must survive, not just type/text/signature
        assert stored["signature"] == "sig123_opaque_blob"
        assert stored["encrypted_content"] == "some_provider_blob"
        assert stored["extra_field"] == "should_not_be_dropped"
        assert stored["thinking"] == "deep thoughts here"

    def test_codex_preserves_encrypted_reasoning(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        msg = SimpleNamespace(
            content="result",
            tool_calls=None,
            reasoning="summary text",
            reasoning_content=None,
            reasoning_details=None,
            codex_reasoning_items=[
                {"type": "reasoning", "id": "rs_1", "encrypted_content": "gAAAA_blob"},
            ],
        )
        result = agent._build_assistant_message(msg, "stop")
        assert result["codex_reasoning_items"] == [
            {"type": "reasoning", "id": "rs_1", "encrypted_content": "gAAAA_blob"},
        ]



# ── Auxiliary client provider resolution ─────────────────────────────────────

class TestAuxiliaryClientProviderPriority:
    """Verify auxiliary client resolution doesn't break for any provider."""

    def test_openrouter_always_wins(self, monkeypatch):
        monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
        from agent.auxiliary_client import _OPENROUTER_MODEL, get_text_auxiliary_client
        with patch("agent.auxiliary_client.OpenAI") as mock:
            client, model = get_text_auxiliary_client()
        assert model == _OPENROUTER_MODEL
        assert "openrouter" in str(mock.call_args.kwargs["base_url"]).lower()

    def test_nous_when_no_openrouter(self, monkeypatch):
        monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
        from agent.auxiliary_client import _NOUS_MODEL, get_text_auxiliary_client
        nous_auth = {
            "access_token": _fake_invoke_jwt(),
            "scope": "inference:invoke",
        }
        with patch("agent.auxiliary_client._read_nous_auth", return_value=nous_auth), \
             patch("agent.auxiliary_client.OpenAI") as mock, \
             patch("hermes_cli.models.get_nous_recommended_aux_model", return_value=None):
            client, model = get_text_auxiliary_client()
        assert model == _NOUS_MODEL

    def test_custom_endpoint_when_no_nous(self, monkeypatch):
        """Custom endpoint is used when no OpenRouter/Nous keys are available.

        Since the March 2026 config refactor, OPENAI_BASE_URL env var is no
        longer consulted — base_url comes from config.yaml via
        resolve_runtime_provider.  Mock _resolve_custom_runtime directly.
        """
        monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
        monkeypatch.setenv("OPENAI_API_KEY", "local-key")
        from agent.auxiliary_client import get_text_auxiliary_client
        with patch("agent.auxiliary_client._read_nous_auth", return_value=None), \
             patch("agent.auxiliary_client._resolve_custom_runtime",
                   return_value=("http://localhost:1234/v1", "local-key")), \
             patch("agent.auxiliary_client.OpenAI") as mock:
            client, model = get_text_auxiliary_client()
        assert mock.call_args.kwargs["base_url"] == "http://localhost:1234/v1"

    def test_codex_not_in_auto_fallback(self, monkeypatch):
        """Codex is deliberately NOT part of the auto fallback chain.

        ChatGPT-account Codex gates which models it accepts via an
        undocumented, shifting allow-list, so falling through to Codex with
        a hardcoded default model breaks silently whenever OpenAI rotates
        the list.  When nothing else is available, ``get_text_auxiliary_client``
        now returns (None, None) rather than guessing a Codex model.
        """
        monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
        monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
        monkeypatch.delenv("OPENAI_API_KEY", raising=False)
        from agent.auxiliary_client import get_text_auxiliary_client
        with patch("agent.auxiliary_client._read_nous_auth", return_value=None), \
             patch("agent.auxiliary_client._read_codex_access_token", return_value="codex-tok"), \
             patch("agent.auxiliary_client.OpenAI"):
            client, model = get_text_auxiliary_client()
        assert client is None
        assert model is None


# ── Provider routing tests ───────────────────────────────────────────────────

class TestProviderRouting:
    """Verify provider_routing config flows into extra_body.provider."""

    def test_sort_throughput(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openrouter")
        agent.provider_sort = "throughput"
        kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}])
        assert kwargs["extra_body"]["provider"]["sort"] == "throughput"






    def test_no_routing_when_unset(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openrouter")
        kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}])
        assert "provider" not in kwargs.get("extra_body", {}).get("provider", {}) or \
               kwargs.get("extra_body", {}).get("provider") is None or \
               "only" not in kwargs.get("extra_body", {}).get("provider", {})




# ── Codex reasoning items preflight tests ────────────────────────────────────

class TestCodexReasoningPreflight:
    """Verify reasoning items pass through preflight normalization."""

    def test_reasoning_item_passes_through(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        raw_input = [
            {"role": "user", "content": "hello"},
            {"type": "reasoning", "encrypted_content": "abc123encrypted", "id": "r_001",
             "summary": [{"type": "summary_text", "text": "Thinking about it"}]},
            {"role": "assistant", "content": "hi there"},
        ]
        normalized = _preflight_codex_input_items(raw_input)
        reasoning_items = [i for i in normalized if i.get("type") == "reasoning"]
        assert len(reasoning_items) == 1
        assert reasoning_items[0]["encrypted_content"] == "abc123encrypted"
        # Note: "id" is intentionally excluded from normalized output —
        # with store=False the API returns 404 on server-side id resolution.
        # The id is only used for local deduplication via seen_ids.
        assert "id" not in reasoning_items[0]
        assert reasoning_items[0]["summary"] == [{"type": "summary_text", "text": "Thinking about it"}]

    def test_reasoning_item_without_id(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        raw_input = [
            {"type": "reasoning", "encrypted_content": "abc123"},
        ]
        normalized = _preflight_codex_input_items(raw_input)
        assert len(normalized) == 1
        assert "id" not in normalized[0]
        assert normalized[0]["summary"] == []  # default empty summary


    def test_reasoning_items_replayed_from_history(self, monkeypatch):
        """Reasoning items stored in codex_reasoning_items get replayed."""
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        messages = [
            {"role": "user", "content": "hello"},
            {
                "role": "assistant",
                "content": "hi",
                "codex_reasoning_items": [
                    {"type": "reasoning", "encrypted_content": "enc123", "id": "r_1"},
                ],
            },
            {"role": "user", "content": "follow up"},
        ]
        items = _chat_messages_to_responses_input(messages)
        reasoning_items = [i for i in items if isinstance(i, dict) and i.get("type") == "reasoning"]
        assert len(reasoning_items) == 1
        assert reasoning_items[0]["encrypted_content"] == "enc123"


# ── Reasoning effort consistency tests ───────────────────────────────────────

class TestReasoningEffortDefaults:
    """Verify reasoning effort defaults to medium across all provider paths."""

    def test_openrouter_default_medium(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openrouter")
        agent.model = "anthropic/claude-sonnet-4-20250514"
        kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}])
        reasoning = kwargs["extra_body"]["reasoning"]
        assert reasoning["effort"] == "medium"

    def test_codex_default_medium(self, monkeypatch):
        agent = _make_agent(monkeypatch, "openai-codex", api_mode="codex_responses",
                            base_url="https://chatgpt.com/backend-api/codex")
        kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}])
        assert kwargs["reasoning"]["effort"] == "medium"



