# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

import pytest
from openai.types.responses import FunctionTool
from openai_harmony import DeveloperContent, Message, Role

from tests.entrypoints.openai.utils import verify_harmony_messages
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionToolsParam
from vllm.entrypoints.openai.parser.harmony_utils import (
    auto_drop_analysis_messages,
    create_tool_definition,
    extract_function_from_recipient,
    get_system_message,
    has_custom_tools,
    is_function_recipient,
    parse_chat_input_to_harmony_message,
)
from vllm.entrypoints.openai.responses.harmony import (
    response_input_to_harmony,
    response_previous_input_to_harmony,
)

_TOOL_PARAMETERS = {
    "type": "object",
    "properties": {"status": {"type": "string"}},
    "required": ["status"],
    "additionalProperties": False,
}


class TestCreateToolDefinition:
    def test_chat_completion_omitted_description_defaults_to_empty_string(self):
        tool = ChatCompletionToolsParam(
            function={
                "name": "report_status",
                "parameters": _TOOL_PARAMETERS,
            }
        )

        tool_definition = create_tool_definition(tool)

        assert tool_definition.name == "report_status"
        assert tool_definition.description == ""
        assert tool_definition.parameters == _TOOL_PARAMETERS

    def test_chat_completion_none_description_defaults_to_empty_string(self):
        tool = ChatCompletionToolsParam(
            function={
                "name": "report_status",
                "description": None,
                "parameters": _TOOL_PARAMETERS,
            }
        )

        tool_definition = create_tool_definition(tool)

        assert tool_definition.name == "report_status"
        assert tool_definition.description == ""
        assert tool_definition.parameters == _TOOL_PARAMETERS

    def test_response_tool_none_description_defaults_to_empty_string(self):
        tool = FunctionTool(
            name="report_status",
            description=None,
            parameters=_TOOL_PARAMETERS,
            type="function",
        )

        tool_definition = create_tool_definition(tool)

        assert tool_definition.name == "report_status"
        assert tool_definition.description == ""
        assert tool_definition.parameters == _TOOL_PARAMETERS


class TestIsFunctionRecipient:
    @pytest.mark.parametrize(
        "recipient",
        [
            "functions.get_weather",
            "functions.search_web",
            "functions.math.sum",
        ],
    )
    def test_functions_prefix_accepted(self, recipient):
        assert is_function_recipient(recipient) is True

    @pytest.mark.parametrize(
        "recipient",
        [
            "get_weather",
            "search_web",
            "calculator",
            "my-tool",
        ],
    )
    def test_bare_function_name_accepted(self, recipient):
        assert is_function_recipient(recipient) is True

    @pytest.mark.parametrize(
        "recipient",
        [
            "assistant",
        ],
    )
    def test_assistant_rejected(self, recipient):
        assert is_function_recipient(recipient) is False

    @pytest.mark.parametrize(
        "recipient",
        [
            "math.sum",
            "code.run",
            "namespace.tool_name",
            "my.deeply.nested.tool",
        ],
    )
    def test_dotted_function_names_accepted(self, recipient):
        assert is_function_recipient(recipient) is True

    @pytest.mark.parametrize(
        "recipient",
        [
            "python",
            "browser",
            "container",
        ],
    )
    def test_builtin_tool_names_rejected(self, recipient):
        assert is_function_recipient(recipient) is False

    @pytest.mark.parametrize(
        "recipient",
        [
            "python.run",
            "python.execute",
            "browser.search",
            "browser.open",
            "container.exec",
        ],
    )
    def test_builtin_dotted_variants_rejected(self, recipient):
        assert is_function_recipient(recipient) is False

    @pytest.mark.parametrize(
        "recipient",
        [
            "",
            "functions.",
        ],
    )
    def test_empty_recipients_rejected(self, recipient):
        assert is_function_recipient(recipient) is False

    @pytest.mark.parametrize(
        "recipient",
        [
            "<|start|>",
            "<|end|>",
            "<|channel|>",
        ],
    )
    def test_harmony_tokens_rejected(self, recipient):
        assert is_function_recipient(recipient) is False


class TestIsFunctionRecipientWithAllowedNames:
    """Tests for is_function_recipient with allowed_function_tool_names."""

    def test_prefixed_always_accepted(self):
        """functions. prefix is always accepted regardless of allowed names."""
        fn_names = frozenset({"other_tool"})
        assert is_function_recipient("functions.get_weather", fn_names) is True

    def test_bare_name_accepted_when_in_allowed_names(self):
        fn_names = frozenset({"get_weather", "search_web"})
        assert is_function_recipient("get_weather", fn_names) is True
        assert is_function_recipient("search_web", fn_names) is True

    def test_bare_name_rejected_when_not_in_allowed_names(self):
        fn_names = frozenset({"get_weather"})
        assert is_function_recipient("unknown_tool", fn_names) is False

    def test_dotted_name_accepted_when_in_allowed_names(self):
        fn_names = frozenset({"math.sum", "namespace.tool_name"})
        assert is_function_recipient("math.sum", fn_names) is True
        assert is_function_recipient("namespace.tool_name", fn_names) is True

    def test_dotted_name_rejected_when_not_in_allowed_names(self):
        fn_names = frozenset({"get_weather"})
        assert is_function_recipient("custom_server.search", fn_names) is False

    def test_empty_allowed_names_rejects_bare_names(self):
        """Empty frozenset means no function tools — bare names are not functions."""
        fn_names: frozenset[str] = frozenset()
        assert is_function_recipient("get_weather", fn_names) is False
        assert is_function_recipient("math.sum", fn_names) is False

    def test_builtin_tools_always_rejected(self):
        fn_names = frozenset({"python", "browser", "container"})
        assert is_function_recipient("python", fn_names) is False
        assert is_function_recipient("browser", fn_names) is False
        assert is_function_recipient("container", fn_names) is False

    def test_builtin_dotted_always_rejected(self):
        fn_names = frozenset({"python.run", "browser.search"})
        assert is_function_recipient("python.run", fn_names) is False
        assert is_function_recipient("browser.search", fn_names) is False

    def test_none_allowed_names_uses_heuristic(self):
        """When allowed names is None (Chat Completions), use heuristic."""
        assert is_function_recipient("get_weather", None) is True
        assert is_function_recipient("math.sum", None) is True
        assert is_function_recipient("python", None) is False


class TestExtractFunctionFromRecipient:
    @pytest.mark.parametrize(
        "recipient,expected",
        [
            ("functions.get_weather", "get_weather"),
            ("functions.search_web", "search_web"),
            ("functions.", ""),
        ],
    )
    def test_strips_functions_prefix(self, recipient, expected):
        assert extract_function_from_recipient(recipient) == expected

    @pytest.mark.parametrize(
        "recipient",
        [
            "get_weather",
            "calculator",
            "my-tool",
        ],
    )
    def test_bare_name_returned_as_is(self, recipient):
        assert extract_function_from_recipient(recipient) == recipient

    @pytest.mark.parametrize(
        "recipient,expected",
        [
            ("functions.math.sum", "math.sum"),
            ("math.sum", "math.sum"),
            ("namespace.tool_name", "namespace.tool_name"),
        ],
    )
    def test_dotted_function_name_extraction(self, recipient, expected):
        assert extract_function_from_recipient(recipient) == expected


class TestCommonParseInputToHarmonyMessage:
    """
    Tests for scenarios that are common to both Chat Completion
    parse_chat_input_to_harmony_message and Responses API
    response_previous_input_to_harmony functions.
    """

    @pytest.fixture(
        params=[parse_chat_input_to_harmony_message, response_previous_input_to_harmony]
    )
    def parse_function(self, request):
        return request.param

    def test_assistant_message_with_tool_calls(self, parse_function):
        """Test parsing assistant message with tool calls."""
        chat_msg = {
            "role": "assistant",
            "tool_calls": [
                {
                    "function": {
                        "name": "get_weather",
                        "arguments": '{"location": "San Francisco"}',
                    }
                },
                {
                    "function": {
                        "name": "search_web",
                        "arguments": '{"query": "latest news"}',
                    }
                },
            ],
        }

        messages = parse_function(chat_msg)

        assert len(messages) == 2

        # First tool call
        assert messages[0].author.role == Role.ASSISTANT
        assert messages[0].content[0].text == '{"location": "San Francisco"}'
        assert messages[0].channel == "commentary"
        assert messages[0].recipient == "functions.get_weather"
        assert messages[0].content_type == "json"

        # Second tool call
        assert messages[1].author.role == Role.ASSISTANT
        assert messages[1].content[0].text == '{"query": "latest news"}'
        assert messages[1].channel == "commentary"
        assert messages[1].recipient == "functions.search_web"
        assert messages[1].content_type == "json"

    def test_assistant_message_with_empty_tool_call_arguments(self, parse_function):
        """Test parsing assistant message with tool call having None arguments."""
        chat_msg = {
            "role": "assistant",
            "tool_calls": [
                {
                    "function": {
                        "name": "get_current_time",
                        "arguments": None,
                    }
                }
            ],
        }

        messages = parse_function(chat_msg)

        assert len(messages) == 1
        assert messages[0].content[0].text == ""
        assert messages[0].recipient == "functions.get_current_time"

    def test_system_message(self, parse_function):
        """Test parsing system messages, which are parsed into developer messages
        with DeveloperContent."""
        chat_msg = {
            "role": "system",
            "content": "You are a helpful assistant",
        }

        messages = parse_function(chat_msg)

        assert len(messages) == 1
        assert messages[0].author.role == Role.DEVELOPER
        assert isinstance(messages[0].content[0], DeveloperContent)
        assert messages[0].content[0].instructions == "You are a helpful assistant"

    def test_developer_message(self, parse_function):
        """Test parsing developer message."""
        chat_msg = {
            "role": "developer",
            "content": "Use concise language",
        }

        messages = parse_function(chat_msg)

        assert len(messages) == 1
        assert messages[0].author.role == Role.DEVELOPER
        assert isinstance(messages[0].content[0], DeveloperContent)
        assert messages[0].content[0].instructions == "Use concise language"

    def test_user_message_with_string_content(self, parse_function):
        """Test parsing user message with string content."""
        chat_msg = {
            "role": "user",
            "content": "What's the weather in San Francisco?",
        }

        messages = parse_function(chat_msg)

        assert len(messages) == 1
        assert messages[0].author.role == Role.USER
        assert messages[0].content[0].text == "What's the weather in San Francisco?"

    def test_user_message_with_array_content(self, parse_function):
        """Test parsing user message with array content."""
        chat_msg = {
            "role": "user",
            "content": [
                {"text": "What's in this image? "},
                {"text": "Please describe it."},
            ],
        }

        messages = parse_function(chat_msg)

        assert len(messages) == 1
        assert messages[0].author.role == Role.USER
        assert len(messages[0].content) == 2
        assert messages[0].content[0].text == "What's in this image? "
        assert messages[0].content[1].text == "Please describe it."

    def test_assistant_message_with_string_content(self, parse_function):
        """Test parsing assistant message with string content (no tool calls)."""
        chat_msg = {
            "role": "assistant",
            "content": "Hello! How can I help you today?",
        }

        messages = parse_function(chat_msg)

        assert len(messages) == 1
        assert messages[0].author.role == Role.ASSISTANT
        assert messages[0].content[0].text == "Hello! How can I help you today?"

    def test_pydantic_model_input(self, parse_function):
        """Test parsing Pydantic model input (has model_dump method)."""

        class MockPydanticModel:
            def model_dump(self, exclude_none=True):
                return {
                    "role": "user",
                    "content": "Test message",
                }

        chat_msg = MockPydanticModel()
        messages = parse_function(chat_msg)

        assert len(messages) == 1
        assert messages[0].author.role == Role.USER
        assert messages[0].content[0].text == "Test message"

    def test_tool_call_with_missing_function_fields(self, parse_function):
        """Test parsing tool call with missing name or arguments."""
        chat_msg = {
            "role": "assistant",
            "tool_calls": [
                {
                    "function": {}  # Missing both name and arguments
                }
            ],
        }

        messages = parse_function(chat_msg)

        assert len(messages) == 1
        assert messages[0].recipient == "functions."
        assert messages[0].content[0].text == ""

    def test_array_content_with_missing_text(self, parse_function):
        """Test parsing array content where text field is missing."""
        chat_msg = {
            "role": "user",
            "content": [
                {},  # Missing text field
                {"text": "actual text"},
            ],
        }

        messages = parse_function(chat_msg)

        assert len(messages) == 1
        assert len(messages[0].content) == 2
        assert messages[0].content[0].text == ""
        assert messages[0].content[1].text == "actual text"


class TestParseChatInputToHarmonyMessage:
    """
    Tests for scenarios that are specific to the Chat Completion API
    parse_chat_input_to_harmony_message function.
    """

    def test_user_message_with_empty_content(self):
        chat_msg = {
            "role": "user",
            "content": "",
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "user",
                    "content": "",
                },
            ],
        )

    def test_user_message_with_none_content(self):
        chat_msg = {
            "role": "user",
            "content": None,
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "user",
                    "content": "",
                },
            ],
        )

    def test_assistant_message_with_empty_content(self):
        chat_msg = {
            "role": "assistant",
            "content": "",
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        assert len(messages) == 0

    def test_assistant_message_with_none_content(self):
        chat_msg = {
            "role": "assistant",
            "content": None,
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        assert len(messages) == 0

    def test_assistant_message_with_content_but_empty_reasoning(self):
        chat_msg = {
            "role": "assistant",
            "content": "The answer is 4.",
            "reasoning": "",
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "assistant",
                    "channel": "final",
                    "content": "The answer is 4.",
                },
            ],
        )

    def test_assistant_message_with_reasoning_but_empty_content(self):
        chat_msg = {
            "role": "assistant",
            "reasoning": "I'm thinking about the user's question.",
            "content": "",
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "assistant",
                    "channel": "analysis",
                    "content": "I'm thinking about the user's question.",
                },
            ],
        )

    def test_assistant_message_with_reasoning_but_none_content(self):
        chat_msg = {
            "role": "assistant",
            "reasoning": "I'm thinking about the user's question.",
            "content": None,
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "assistant",
                    "channel": "analysis",
                    "content": "I'm thinking about the user's question.",
                },
            ],
        )

    def test_assistant_message_with_tool_calls_but_no_content(self):
        chat_msg = {
            "role": "assistant",
            "tool_calls": [
                {
                    "function": {
                        "name": "get_weather",
                        "arguments": '{"location": "San Francisco"}',
                    }
                }
            ],
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "assistant",
                    "channel": "commentary",
                    "recipient": "functions.get_weather",
                    "content": '{"location": "San Francisco"}',
                    "content_type": "json",
                },
            ],
        )

    def test_assistant_message_with_tool_calls_and_content(self):
        chat_msg = {
            "role": "assistant",
            "tool_calls": [
                {
                    "function": {
                        "name": "get_weather",
                        "arguments": '{"location": "San Francisco"}',
                    }
                }
            ],
            "content": "I'll call the tool.",
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "assistant",
                    "channel": "commentary",
                    "content": "I'll call the tool.",
                },
                {
                    "role": "assistant",
                    "channel": "commentary",
                    "recipient": "functions.get_weather",
                    "content": '{"location": "San Francisco"}',
                    "content_type": "json",
                },
            ],
        )

    def test_assistant_message_with_tool_calls_and_reasoning(self):
        chat_msg = {
            "role": "assistant",
            "tool_calls": [
                {
                    "function": {
                        "name": "get_weather",
                        "arguments": '{"location": "San Francisco"}',
                    }
                }
            ],
            "reasoning": "I should use the get_weather tool.",
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "assistant",
                    "channel": "analysis",
                    "content": "I should use the get_weather tool.",
                },
                {
                    "role": "assistant",
                    "channel": "commentary",
                    "recipient": "functions.get_weather",
                    "content": '{"location": "San Francisco"}',
                    "content_type": "json",
                },
            ],
        )

    def test_assistant_message_with_tool_calls_and_reasoning_and_content(self):
        chat_msg = {
            "role": "assistant",
            "tool_calls": [
                {
                    "function": {
                        "name": "get_weather",
                        "arguments": '{"location": "San Francisco"}',
                    }
                }
            ],
            "reasoning": "I should use the get_weather tool.",
            "content": "I'll call the tool.",
        }

        messages = parse_chat_input_to_harmony_message(chat_msg)

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "assistant",
                    "channel": "commentary",
                    "content": "I'll call the tool.",
                },
                {
                    "role": "assistant",
                    "channel": "analysis",
                    "content": "I should use the get_weather tool.",
                },
                {
                    "role": "assistant",
                    "channel": "commentary",
                    "recipient": "functions.get_weather",
                    "content": '{"location": "San Francisco"}',
                    "content_type": "json",
                },
            ],
        )

    def test_tool_message_with_string_content(self):
        tool_id_names = {
            "call_123": "get_weather",
        }
        chat_msg = {
            "role": "tool",
            "tool_call_id": "call_123",
            "content": "The weather in San Francisco is sunny, 72°F",
        }

        messages = parse_chat_input_to_harmony_message(
            chat_msg, tool_id_names=tool_id_names
        )

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "tool",
                    "name": "functions.get_weather",
                    "content": "The weather in San Francisco is sunny, 72°F",
                    "channel": "commentary",
                },
            ],
        )

    def test_tool_message_with_array_content(self):
        tool_id_names = {
            "call_123": "search_results",
        }
        chat_msg = {
            "role": "tool",
            "tool_call_id": "call_123",
            "content": [
                {"type": "text", "text": "Result 1: "},
                {"type": "text", "text": "Result 2: "},
                {
                    "type": "image",
                    "url": "http://example.com/img.png",
                },  # Should be ignored
                {"type": "text", "text": "Result 3"},
            ],
        }

        messages = parse_chat_input_to_harmony_message(
            chat_msg, tool_id_names=tool_id_names
        )

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "tool",
                    "name": "functions.search_results",
                    "content": "Result 1: Result 2: Result 3",
                    "channel": "commentary",
                },
            ],
        )

    def test_tool_message_with_empty_content(self):
        tool_id_names = {
            "call_123": "empty_tool",
        }
        chat_msg = {
            "role": "tool",
            "tool_call_id": "call_123",
            "content": "",
        }

        messages = parse_chat_input_to_harmony_message(
            chat_msg, tool_id_names=tool_id_names
        )

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "tool",
                    "name": "functions.empty_tool",
                    "content": "",
                    "channel": "commentary",
                },
            ],
        )

    def test_tool_message_with_none_content(self):
        tool_id_names = {
            "call_123": "empty_tool",
        }
        chat_msg = {
            "role": "tool",
            "tool_call_id": "call_123",
            "content": None,
        }

        messages = parse_chat_input_to_harmony_message(
            chat_msg, tool_id_names=tool_id_names
        )

        verify_harmony_messages(
            messages,
            [
                {
                    "role": "tool",
                    "name": "functions.empty_tool",
                    "content": "",
                    "channel": "commentary",
                },
            ],
        )


class TestAutoDropAnalysisMessages:
    def test_no_analysis_messages(self) -> None:
        messages = [
            Message.from_role_and_content(
                Role.ASSISTANT, "The answer is 4."
            ).with_channel("final"),
        ]
        cleaned_messages = auto_drop_analysis_messages(messages)
        assert cleaned_messages == messages

    def test_only_analysis_message(self) -> None:
        messages = [
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking about the user's question."
            ).with_channel("analysis"),
        ]
        cleaned_messages = auto_drop_analysis_messages(messages)
        assert cleaned_messages == messages

    def test_multiple_analysis_messages_without_final_message(self) -> None:
        messages = [
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking about the user's question."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking more."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking even more."
            ).with_channel("analysis"),
        ]
        cleaned_messages = auto_drop_analysis_messages(messages)
        assert cleaned_messages == messages

    def test_only_final_message(self) -> None:
        messages = [
            Message.from_role_and_content(
                Role.ASSISTANT, "The answer is 4."
            ).with_channel("final"),
        ]
        cleaned_messages = auto_drop_analysis_messages(messages)
        assert cleaned_messages == messages

    def test_drops_one_analysis_messages_before_final_message(self) -> None:
        messages = [
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking about the user's question."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "The answer is 4."
            ).with_channel("final"),
            Message.from_role_and_content(
                Role.ASSISTANT, "I should think harder."
            ).with_channel("analysis"),
        ]
        cleaned_messages = auto_drop_analysis_messages(messages)
        # Should have dropped the first analysis message
        assert cleaned_messages == messages[1:]

    def test_drops_all_analysis_messages_before_final_message(self) -> None:
        messages = [
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking about the user's question."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking more."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking even more."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "The answer is 4."
            ).with_channel("final"),
            Message.from_role_and_content(
                Role.ASSISTANT, "I should think harder."
            ).with_channel("analysis"),
        ]
        cleaned_messages = auto_drop_analysis_messages(messages)
        # Should have dropped the first 3 analysis messages
        assert cleaned_messages == messages[3:]

    def test_multiple_analysis_messages_with_multiple_final_messages(self) -> None:
        messages = [
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking about the user's question."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking more."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "I'm thinking even more."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "The answer is 4."
            ).with_channel("final"),
            Message.from_role_and_content(
                Role.ASSISTANT, "I should think harder."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "The answer is 5."
            ).with_channel("final"),
        ]
        cleaned_messages = auto_drop_analysis_messages(messages)
        # Should have dropped all those analysis messages
        assert len(cleaned_messages) == 2
        assert cleaned_messages[0].content[0].text == "The answer is 4."
        assert cleaned_messages[1].content[0].text == "The answer is 5."

    def test_drops_non_assistant_analysis_messages(self) -> None:
        messages = [
            Message.from_role_and_content(
                Role.TOOL, "The tool thinks we should think harder."
            ).with_channel("analysis"),
            Message.from_role_and_content(
                Role.ASSISTANT, "The answer is 4."
            ).with_channel("final"),
        ]
        cleaned_messages = auto_drop_analysis_messages(messages)
        # Should have dropped the analysis message
        assert cleaned_messages == messages[1:]


def test_has_custom_tools() -> None:
    assert not has_custom_tools(set())
    assert not has_custom_tools({"web_search_preview", "code_interpreter", "container"})
    assert has_custom_tools({"others"})
    assert has_custom_tools(
        {"web_search_preview", "code_interpreter", "container", "others"}
    )


class TestGetSystemMessage:
    """Tests for get_system_message channel configuration."""

    def test_commentary_channel_present_without_custom_tools(self) -> None:
        """Commentary channel must be valid even without custom tools."""
        sys_msg = get_system_message(with_custom_tools=False)
        valid_channels = sys_msg.content[0].channel_config.valid_channels
        assert "commentary" in valid_channels

    def test_commentary_channel_present_with_custom_tools(self) -> None:
        """Commentary channel present when custom tools are enabled."""
        sys_msg = get_system_message(with_custom_tools=True)
        valid_channels = sys_msg.content[0].channel_config.valid_channels
        assert "commentary" in valid_channels

    def test_all_standard_channels_present(self) -> None:
        """All three standard Harmony channels should always be valid."""
        for with_tools in (True, False):
            sys_msg = get_system_message(with_custom_tools=with_tools)
            valid_channels = sys_msg.content[0].channel_config.valid_channels
            for channel in ("analysis", "commentary", "final"):
                assert channel in valid_channels, (
                    f"{channel} missing when with_custom_tools={with_tools}"
                )

    def test_unsupported_reasoning_effort_raises_clear_error(self) -> None:
        with pytest.raises(
            ValueError,
            match="reasoning_effort='max' is not supported by Harmony",
        ):
            get_system_message(reasoning_effort="max")


class TestResponseInputToHarmonyReasoningItem:
    """Tests for response_input_to_harmony handling of reasoning input items.

    Per the OpenAI spec, ResponseReasoningItem.content is
    Optional[List[Content]] = None. Clients like langchain-openai may omit
    this field when constructing multi-turn input from previous responses.

    Reasoning items with content are converted to Harmony messages on the
    'analysis' channel. All content items are concatenated. Items without
    content return None (skipped by the caller).
    """

    def test_reasoning_with_single_content(self):
        """Test reasoning item with a single content entry."""
        item = {
            "type": "reasoning",
            "id": "rs_123",
            "content": [{"type": "reasoning_text", "text": "Thinking step by step"}],
        }

        msg = response_input_to_harmony(item, prev_responses=[])

        assert msg is not None
        assert msg.author.role == Role.ASSISTANT
        assert msg.content[0].text == "Thinking step by step"
        assert msg.channel == "analysis"

    def test_reasoning_with_multiple_content_items(self):
        """Test reasoning item with multiple content entries concatenated."""
        item = {
            "type": "reasoning",
            "id": "rs_123",
            "content": [
                {"type": "reasoning_text", "text": "First, let me analyze"},
                {"type": "reasoning_text", "text": "Second, I should consider"},
                {"type": "reasoning_text", "text": "Finally, the answer is"},
            ],
        }

        msg = response_input_to_harmony(item, prev_responses=[])

        assert msg is not None
        assert msg.author.role == Role.ASSISTANT
        assert msg.content[0].text == (
            "First, let me analyze\nSecond, I should consider\nFinally, the answer is"
        )
        assert msg.channel == "analysis"

    def test_reasoning_without_content_returns_none(self):
        """Test reasoning item without content field returns None."""
        item = {
            "type": "reasoning",
            "id": "rs_123",
            "summary": [{"type": "summary_text", "text": "Thinking about math"}],
        }

        msg = response_input_to_harmony(item, prev_responses=[])

        assert msg is None

    def test_reasoning_with_none_content_returns_none(self):
        """Test reasoning item with content=None returns None."""
        item = {
            "type": "reasoning",
            "id": "rs_123",
            "content": None,
            "summary": [{"type": "summary_text", "text": "Thinking about math"}],
        }

        msg = response_input_to_harmony(item, prev_responses=[])

        assert msg is None

    def test_reasoning_with_empty_content_returns_none(self):
        """Test reasoning item with empty content list returns None."""
        item = {
            "type": "reasoning",
            "id": "rs_123",
            "content": [],
        }

        msg = response_input_to_harmony(item, prev_responses=[])

        assert msg is None
