# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for ResponsesParser with the unified Parser interface.

These tests verify that ResponsesParser correctly delegates to the unified
Parser (via extract_response_outputs) instead of calling separate
ReasoningParser / ToolParser instances directly.
"""

from collections.abc import Sequence
from unittest.mock import MagicMock

import pytest

from vllm.entrypoints.openai.engine.protocol import (
    DeltaMessage,
    ExtractedToolCallInformation,
    FunctionCall,
    ToolCall,
)
from vllm.entrypoints.openai.parser.responses_parser import (
    ResponsesParser,
    get_responses_parser_for_simple_context,
)
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
from vllm.outputs import CompletionOutput
from vllm.parser.abstract_parser import DelegatingParser

pytestmark = pytest.mark.skip_global_cleanup


# ---------------------------------------------------------------------------
# Test parser stubs
# ---------------------------------------------------------------------------


class _NoOpParser(DelegatingParser):
    """Parser that extracts no reasoning and no tool calls."""

    def is_reasoning_end(self, input_ids: list[int]) -> bool:
        return False

    def extract_content_ids(self, input_ids: list[int]) -> list[int]:
        return input_ids

    def extract_reasoning(self, model_output, request):
        return None, model_output

    def extract_reasoning_streaming(self, *args, **kwargs):
        return None

    def extract_tool_calls(self, model_output, request):
        return ExtractedToolCallInformation(
            tools_called=False, tool_calls=[], content=model_output
        )

    def extract_tool_calls_streaming(self, *args, **kwargs):
        return None

    def parse_delta(self, *args, **kwargs) -> DeltaMessage | None:
        return None


class _ReasoningOnlyParser(DelegatingParser):
    """Parser that extracts reasoning but no tool calls."""

    def is_reasoning_end(self, input_ids: list[int]) -> bool:
        return False

    def extract_content_ids(self, input_ids: list[int]) -> list[int]:
        return input_ids

    def extract_reasoning(self, model_output, request):
        if "<think>" in model_output and "</think>" in model_output:
            start = model_output.index("<think>") + len("<think>")
            end = model_output.index("</think>")
            reasoning = model_output[start:end]
            content = model_output[end + len("</think>") :]
            return reasoning, content.strip() or None
        return None, model_output

    def extract_reasoning_streaming(self, *args, **kwargs):
        return None

    def extract_tool_calls(self, model_output, request):
        return ExtractedToolCallInformation(
            tools_called=False, tool_calls=[], content=model_output
        )

    def extract_tool_calls_streaming(self, *args, **kwargs):
        return None

    def parse_delta(self, *args, **kwargs) -> DeltaMessage | None:
        return None


class _StubToolParser:
    """Minimal tool parser stub that always returns a hardcoded tool call."""

    supports_required_and_named = False

    def __init__(self, tokenizer=None, tools=None):
        pass

    def extract_tool_calls(self, model_output, request):
        return ExtractedToolCallInformation(
            tools_called=True,
            tool_calls=[
                ToolCall(
                    id="call_123",
                    type="function",
                    function=FunctionCall(
                        name="get_weather",
                        arguments='{"location": "Paris"}',
                    ),
                )
            ],
            content=None,
        )

    def extract_tool_calls_streaming(self, *args, **kwargs):
        return None

    def adjust_request(self, request):
        return request


class _ToolCallingParser(DelegatingParser):
    """Parser that extracts a hardcoded tool call from any input."""

    def __init__(self, tokenizer, *args, **kwargs):
        super().__init__(tokenizer)
        self._tool_parser = _StubToolParser()

    def is_reasoning_end(self, input_ids: list[int]) -> bool:
        return False

    def extract_content_ids(self, input_ids: list[int]) -> list[int]:
        return input_ids

    def extract_reasoning(self, model_output, request):
        return None, model_output

    def extract_reasoning_streaming(self, *args, **kwargs):
        return None

    def extract_tool_calls_streaming(self, *args, **kwargs):
        return None

    def parse_delta(self, *args, **kwargs) -> DeltaMessage | None:
        return None


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_request(**overrides) -> ResponsesRequest:
    defaults = {"model": "test-model", "input": "test"}
    defaults.update(overrides)
    return ResponsesRequest.model_validate(defaults)


def _make_output(
    text: str = "Hello, world!",
    token_ids: Sequence[int] = (1, 2, 3),
    finish_reason: str = "stop",
) -> CompletionOutput:
    return CompletionOutput(
        index=0,
        text=text,
        token_ids=list(token_ids),
        cumulative_logprob=None,
        logprobs=None,
        finish_reason=finish_reason,
    )


def _make_parser(parser_cls, **overrides):
    defaults = dict(
        tokenizer=MagicMock(),
        parser_cls=parser_cls,
        response_messages=[],
        request=_make_request(),
        chat_template=None,
        chat_template_content_format="auto",
    )
    defaults.update(overrides)
    return ResponsesParser(**defaults)


# ---------------------------------------------------------------------------
# Tests: basic text passthrough
# ---------------------------------------------------------------------------


def test_process_text_with_parser():
    """Parser with no reasoning/tools returns a single message item."""
    parser = _make_parser(_NoOpParser)
    parser.process(_make_output(text="Hello!"))

    assert len(parser.response_messages) == 1
    msg = parser.response_messages[0]
    assert msg.type == "message"
    assert msg.content[0].text == "Hello!"


def test_process_text_without_parser():
    """parser_cls=None falls back to plain text wrapping."""
    parser = _make_parser(None)
    parser.process(_make_output(text="Hello!"))

    assert len(parser.response_messages) == 1
    msg = parser.response_messages[0]
    assert msg.type == "message"
    assert msg.content[0].text == "Hello!"


# ---------------------------------------------------------------------------
# Tests: empty / whitespace output
# ---------------------------------------------------------------------------


def test_process_empty_text_without_parser():
    """Empty text with no parser produces no output items."""
    parser = _make_parser(None)
    parser.process(_make_output(text=""))

    assert len(parser.response_messages) == 0


def test_process_empty_text_with_parser():
    """Empty text with parser produces no output items."""
    parser = _make_parser(_NoOpParser)
    parser.process(_make_output(text=""))

    assert len(parser.response_messages) == 0


# ---------------------------------------------------------------------------
# Tests: reasoning extraction
# ---------------------------------------------------------------------------


def test_process_extracts_reasoning():
    """Parser that finds reasoning produces both reasoning and message items."""
    parser = _make_parser(_ReasoningOnlyParser)
    parser.process(_make_output(text="<think>Let me check</think>The answer is 42"))

    types = [m.type for m in parser.response_messages]
    assert "reasoning" in types
    assert "message" in types

    reasoning_item = next(m for m in parser.response_messages if m.type == "reasoning")
    assert reasoning_item.content[0].text == "Let me check"

    message_item = next(m for m in parser.response_messages if m.type == "message")
    assert message_item.content[0].text == "The answer is 42"


def test_process_reasoning_only_no_content():
    """When reasoning consumes all text, only a reasoning item is produced."""
    parser = _make_parser(_ReasoningOnlyParser)
    parser.process(_make_output(text="<think>Just thinking</think>"))

    types = [m.type for m in parser.response_messages]
    assert "reasoning" in types
    assert "message" not in types


# ---------------------------------------------------------------------------
# Tests: tool call extraction
# ---------------------------------------------------------------------------


def test_process_extracts_tool_calls():
    """Parser that finds tool calls produces function_call items."""
    request = _make_request(
        tool_choice="auto",
        tools=[
            {
                "type": "function",
                "name": "get_weather",
                "parameters": {"type": "object", "properties": {}},
            }
        ],
    )
    parser = _make_parser(_ToolCallingParser, request=request, enable_auto_tools=True)
    parser.process(_make_output(text="calling tool"))

    types = [m.type for m in parser.response_messages]
    assert "function_call" in types

    tool_item = next(m for m in parser.response_messages if m.type == "function_call")
    assert tool_item.name == "get_weather"
    assert tool_item.arguments == '{"location": "Paris"}'
    assert tool_item.status == "completed"


# ---------------------------------------------------------------------------
# Tests: finish_reason tracking
# ---------------------------------------------------------------------------


def test_finish_reason_tracked():
    """finish_reason from CompletionOutput is stored on the parser."""
    parser = _make_parser(_NoOpParser)
    assert parser.finish_reason is None

    parser.process(_make_output(finish_reason="stop"))
    assert parser.finish_reason == "stop"

    parser.process(_make_output(finish_reason="length"))
    assert parser.finish_reason == "length"


# ---------------------------------------------------------------------------
# Tests: multi-turn accumulation
# ---------------------------------------------------------------------------


def test_multi_turn_accumulation():
    """Multiple process() calls accumulate response_messages."""
    parser = _make_parser(_NoOpParser)

    parser.process(_make_output(text="First turn"))
    parser.process(_make_output(text="Second turn"))

    assert len(parser.response_messages) == 2
    texts = [m.content[0].text for m in parser.response_messages]
    assert texts == ["First turn", "Second turn"]


def test_num_init_messages_offset():
    """Initial messages are preserved and offset works correctly."""
    init_messages = [MagicMock(type="message")]
    parser = _make_parser(_NoOpParser, response_messages=init_messages)

    assert parser.num_init_messages == 1

    parser.process(_make_output(text="New output"))

    assert len(parser.response_messages) == 2
    items = parser.make_response_output_items_from_parsable_context()
    assert len(items) == 1
    assert items[0].type == "message"


# ---------------------------------------------------------------------------
# Tests: factory function
# ---------------------------------------------------------------------------


def test_factory_function_creates_parser():
    """get_responses_parser_for_simple_context returns a working parser."""
    rp = get_responses_parser_for_simple_context(
        tokenizer=MagicMock(),
        parser_cls=_NoOpParser,
        response_messages=[],
        request=_make_request(),
        chat_template=None,
        chat_template_content_format="auto",
    )
    assert isinstance(rp, ResponsesParser)

    rp.process(_make_output(text="Works!"))
    assert len(rp.response_messages) == 1


def test_factory_function_none_parser():
    """Factory function works with parser_cls=None."""
    rp = get_responses_parser_for_simple_context(
        tokenizer=MagicMock(),
        parser_cls=None,
        response_messages=[],
        request=_make_request(),
        chat_template=None,
        chat_template_content_format="auto",
    )
    assert isinstance(rp, ResponsesParser)
    assert rp.parser_instance is None
