"""
Regression tests for Codex provider tool_choice normalization.

The reflect agent forces tool selection via named tool_choice dicts on early iterations:
  {"type": "function", "function": {"name": "recall"}}

The Codex Responses API expects the function name at the top level instead:
  {"type": "function", "name": "recall"}

Without normalization, Codex rejects the request with:
  400 Unknown parameter: 'tool_choice.function'
"""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from hindsight_api.engine.providers.codex_llm import CodexLLM

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "recall",
            "description": "Recall semantic memories",
            "parameters": {
                "type": "object",
                "properties": {"query": {"type": "string"}},
                "required": ["query"],
            },
        },
    }
]


def build_llm() -> CodexLLM:
    with patch.object(CodexLLM, "_load_codex_auth", return_value=("token", "account")):
        return CodexLLM(
            provider="openai-codex",
            api_key="ignored",
            base_url="https://chatgpt.com/backend-api",
            model="gpt-5.4-mini",
        )


@pytest.mark.asyncio
async def test_codex_normalizes_legacy_named_tool_choice_shape():
    llm = build_llm()
    response = MagicMock()
    response.status_code = 200
    response.raise_for_status.return_value = None
    with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post:
        mock_post.return_value = response
        with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse:
            mock_parse.return_value = (None, [])
            await llm.call_with_tools(
                messages=[{"role": "user", "content": "recall the memory"}],
                tools=TOOLS,
                tool_choice={"type": "function", "function": {"name": "recall"}},
                max_retries=0,
            )
        sent_payload = mock_post.call_args.kwargs["json"]

    assert sent_payload["tool_choice"] == {"type": "function", "name": "recall"}


@pytest.mark.asyncio
async def test_codex_forced_tool_choice_still_yields_tool_calls():
    llm = build_llm()
    response = MagicMock()
    response.status_code = 200
    response.raise_for_status.return_value = None
    tool_call = {"id": "call-1", "name": "recall", "arguments": {"query": "memory"}}
    with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post:
        mock_post.return_value = response
        with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse:
            mock_parse.return_value = (None, [tool_call])
            result = await llm.call_with_tools(
                messages=[{"role": "user", "content": "recall the memory"}],
                tools=TOOLS,
                tool_choice={"type": "function", "function": {"name": "recall"}},
                max_retries=0,
            )
        sent_payload = mock_post.call_args.kwargs["json"]

    assert len(result.tool_calls) == 1
    assert result.tool_calls[0].name == "recall"
    assert sent_payload["tool_choice"] == {"type": "function", "name": "recall"}
