# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Test function call parsing in ResponsesRequest."""

import json

import pytest
from openai.types.responses import ResponseFunctionToolCall

from vllm.entrypoints.openai.protocol import ResponsesRequest


def test_function_call_dict_converted_to_object():
    """Test that function_call dictionaries are correctly parsed into
    ResponseFunctionToolCall objects."""
    # Create a request with function_call as dict
    request_data = {
        "model": "gpt-oss",
        "input": [
            {
                "type": "function_call",
                "call_id": "fc_123",
                "name": "get_weather",
                "arguments": '{"location": "Boston", "unit": "celsius"}',
            }
        ],
    }

    request = ResponsesRequest(**request_data)

    # Verify the input item is now a ResponseFunctionToolCall object
    assert len(request.input) == 1
    assert isinstance(request.input[0], ResponseFunctionToolCall)
    assert request.input[0].call_id == "fc_123"
    assert request.input[0].name == "get_weather"
    assert request.input[0].arguments == '{"location": "Boston", "unit": "celsius"}'


def test_direct_function_call_object_preservation():
    """Test that ResponseFunctionToolCall objects passed directly are preserved."""
    # Create a request with ResponseFunctionToolCall object
    function_call = ResponseFunctionToolCall(
        type="function_call",
        call_id="fc_456",
        name="get_stock_price",
        arguments='{"symbol": "AAPL"}',
    )

    request_data = {"model": "gpt-oss", "input": [function_call]}

    request = ResponsesRequest(**request_data)

    # Verify the object is preserved
    assert len(request.input) == 1
    assert request.input[0] is function_call


def test_mixed_input_types_with_function_calls():
    """Test parsing with mixed input types including function calls."""

    request_data = {
        "model": "gpt-oss",
        "input": [
            # Valid Message type
            {
                "type": "message",
                "role": "user",
                "content": [{"type": "input_text", "text": "What's the weather?"}],
            },
            # Function call that should be parsed
            {
                "type": "function_call",
                "call_id": "fc_789",
                "name": "check_weather",
                "arguments": '{"location": "NYC"}',
            },
            # Another function call
            {
                "type": "function_call",
                "call_id": "fc_790",
                "name": "get_time",
                "arguments": "{}",
            },
        ],
    }

    request = ResponsesRequest(**request_data)

    # Verify mixed types are handled correctly
    assert len(request.input) == 3
    # First item should be validated as Message
    assert request.input[0]["type"] == "message"
    # Second item should be parsed to ResponseFunctionToolCall
    assert isinstance(request.input[1], ResponseFunctionToolCall)
    assert request.input[1].call_id == "fc_789"
    assert request.input[1].name == "check_weather"
    # Third item should also be parsed to ResponseFunctionToolCall
    assert isinstance(request.input[2], ResponseFunctionToolCall)
    assert request.input[2].call_id == "fc_790"
    assert request.input[2].name == "get_time"


def test_function_call_with_complex_arguments():
    """Test parsing function calls with complex nested arguments."""
    complex_args = {
        "query": "weather forecast",
        "filters": {
            "location": {"city": "San Francisco", "state": "CA"},
            "timeRange": {"start": "2024-01-01", "end": "2024-01-07"},
            "metrics": ["temperature", "humidity", "precipitation"],
        },
        "options": {"format": "detailed", "includeAlerts": True},
    }

    request_data = {
        "model": "gpt-oss",
        "input": [
            {
                "type": "function_call",
                "call_id": "fc_complex",
                "name": "advanced_weather_query",
                "arguments": json.dumps(complex_args),
            }
        ],
    }

    request = ResponsesRequest(**request_data)

    # Verify complex arguments are preserved correctly
    assert len(request.input) == 1
    assert isinstance(request.input[0], ResponseFunctionToolCall)
    assert request.input[0].call_id == "fc_complex"
    assert request.input[0].name == "advanced_weather_query"

    # Parse the arguments back to verify they're intact
    parsed_args = json.loads(request.input[0].arguments)
    assert parsed_args == complex_args


def test_invalid_function_call_fallback():
    """Test that invalid function call dictionaries fall back gracefully."""
    # Missing required field 'call_id'
    request_data = {
        "model": "gpt-oss",
        "input": [
            {"type": "function_call", "name": "incomplete_function", "arguments": "{}"}
        ],
    }

    # This should not raise an error during model creation
    # The validator should keep the original dict and let Pydantic
    # handle validation
    with pytest.raises(ValueError):
        # Pydantic should raise a validation error for the invalid structure
        ResponsesRequest(**request_data)


def test_string_input_not_affected():
    """Test that string input is not affected by the validator."""
    request_data = {"model": "gpt-oss", "input": "This is a simple string input"}

    request = ResponsesRequest(**request_data)

    # Verify string input remains unchanged
    assert request.input == "This is a simple string input"


def test_empty_list_input():
    """Test that empty list input is handled correctly."""
    request_data = {"model": "gpt-oss", "input": []}

    request = ResponsesRequest(**request_data)

    # Verify empty list is preserved
    assert request.input == []


def test_function_call_output_not_affected():
    """Test that FunctionCallOutput is not affected by the function_call parsing."""

    # Test with FunctionCallOutput as dict (should not be parsed)
    request_data = {
        "model": "gpt-oss",
        "input": [
            {
                "type": "function_call_output",
                "call_id": "fc_output_123",
                "output": "The weather in Boston is 72°F and sunny.",
            }
        ],
    }

    request = ResponsesRequest(**request_data)

    # FunctionCallOutput should remain as dict (not converted to an object)
    assert len(request.input) == 1
    assert isinstance(request.input[0], dict)
    assert request.input[0]["type"] == "function_call_output"
    assert request.input[0]["call_id"] == "fc_output_123"
    assert request.input[0]["output"] == "The weather in Boston is 72°F and sunny."


def test_mixed_function_call_and_output():
    """Test that function_call is parsed while function_call_output is preserved."""
    request_data = {
        "model": "gpt-oss",
        "input": [
            # This should be parsed to ResponseFunctionToolCall
            {
                "type": "function_call",
                "call_id": "fc_call_456",
                "name": "get_weather",
                "arguments": '{"location": "NYC"}',
            },
            # This should remain as dict
            {
                "type": "function_call_output",
                "call_id": "fc_call_456",
                "output": "NYC weather is 68°F with light rain",
            },
        ],
    }

    request = ResponsesRequest(**request_data)

    assert len(request.input) == 2

    # First item should be parsed to ResponseFunctionToolCall
    assert isinstance(request.input[0], ResponseFunctionToolCall)
    assert request.input[0].call_id == "fc_call_456"
    assert request.input[0].name == "get_weather"

    # Second item should remain as dict (FunctionCallOutput)
    assert isinstance(request.input[1], dict)
    assert request.input[1]["type"] == "function_call_output"
    assert request.input[1]["call_id"] == "fc_call_456"
    assert request.input[1]["output"] == "NYC weather is 68°F with light rain"


def test_function_call_validation_failure_logs_debug(caplog):
    """Test that validation failures are logged at debug level."""
    from unittest.mock import patch

    request_data = {
        "model": "gpt-oss",
        "input": [
            {
                "type": "function_call",
                "name": "incomplete_function",
                "arguments": "{}",  # Missing call_id
            }
        ],
    }

    # Mock the logger to verify debug was called
    with patch("vllm.entrypoints.openai.protocol.logger") as mock_logger:
        with pytest.raises(ValueError):
            ResponsesRequest(**request_data)

        # Verify debug was called with expected message
        mock_logger.debug.assert_called_once()
        call_args = mock_logger.debug.call_args[0][0]
        assert "Failed to parse function_call" in call_args


def test_validator_handles_iterator_input():
    """Test that validator can handle ValidatorIterator input (Pydantic internal)."""

    # This test simulates when Pydantic passes a ValidatorIterator instead of a list
    # This happened with complex nested structures containing reasoning + function_call

    # Create test data that would normally be a list
    test_input_items = [
        {
            "type": "message",
            "role": "user",
            "content": [{"type": "input_text", "text": "Test"}],
        },
        {
            "type": "reasoning",
            "id": "rs_1",
            "summary": [{"type": "summary_text", "text": "Test reasoning"}],
            "content": [{"type": "reasoning_text", "text": "Test content"}],
        },
        {
            "type": "function_call",
            "call_id": "call_1",
            "name": "test_function",
            "arguments": '{"test": "value"}',
            "id": "fc_1",
        },
    ]

    # Mock data where input is an iterator (simulates Pydantic ValidatorIterator)
    mock_data = {
        "model": "test-model",
        "input": iter(test_input_items),  # Iterator instead of list
    }

    # This should NOT raise an error with the fixed validator
    try:
        request = ResponsesRequest(**mock_data)

        # Verify the validator processed the data correctly
        assert len(request.input) == 3

        # Verify function_call was converted to ResponseFunctionToolCall object
        function_call_item = None
        for item in request.input:
            if isinstance(item, ResponseFunctionToolCall):
                function_call_item = item
                break

        assert function_call_item is not None
        assert function_call_item.call_id == "call_1"
        assert function_call_item.name == "test_function"

    except Exception as e:
        pytest.fail(f"Validator should handle iterator input, but failed with: {e}")


def test_validator_handles_empty_iterator():
    """Test validator handles empty iterator gracefully."""
    mock_data = {
        "model": "test-model",
        "input": iter([]),  # Empty iterator
    }

    request = ResponsesRequest(**mock_data)
    assert request.input == []
