import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from unittest.mock import AsyncMock, MagicMock

import pytest

import litellm
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
from litellm.llms.anthropic.chat.handler import ModelResponseIterator, make_call
from litellm.types.llms.openai import (
    ChatCompletionToolCallChunk,
    ChatCompletionToolCallFunctionChunk,
)
from litellm.types.responses.main import OutputCodeInterpreterCall


@pytest.mark.asyncio
async def test_make_call_passes_logging_obj_to_client_post():
    """make_call must pass logging_obj to client.post so track_llm_api_timing can set llm_api_duration_ms for litellm_overhead_time_ms."""
    mock_client = AsyncMock()
    mock_response = MagicMock()
    mock_response.aiter_lines = MagicMock(
        return_value=iter(
            [b'data: {"type":"message_start"}\n', b'data: {"type":"message_delta"}\n']
        )
    )
    mock_client.post.return_value = mock_response

    logging_obj = MagicMock()

    await make_call(
        client=mock_client,
        api_base="https://api.anthropic.com/v1/messages",
        headers={},
        data="{}",
        model="claude-3-5-haiku",
        messages=[{"role": "user", "content": "Hi"}],
        logging_obj=logging_obj,
        timeout=60.0,
        json_mode=False,
    )

    mock_client.post.assert_called_once()
    call_kwargs = mock_client.post.call_args[1]
    assert call_kwargs.get("logging_obj") is logging_obj


def test_redacted_thinking_content_block_delta():
    chunk = {
        "type": "content_block_start",
        "index": 58,
        "content_block": {
            "type": "redacted_thinking",
            "data": "EuoBCoYBGAIiQJ/SxkPAgqxhKok29YrpJHRUJ0OT8ahCHKAwyhmRuUhtdmDX9+mn4gDzKNv3fVpQdB01zEPMzNY3QuTCd+1bdtEqQK6JuKHqdndbwpr81oVWb4wxd1GqF/7Jkw74IlQa27oobX+KuRkopr9Dllt/RDe7Se0sI1IkU7tJIAQCoP46OAwSDF51P09q67xhHlQ3ihoM2aOVlkghq/X0w8NlIjBMNvXYNbjhyrOcIg6kPFn2ed/KK7Cm5prYAtXCwkb4Wr5tUSoSHu9T5hKdJRbr6WsqEc7Lle7FULqMLZGkhqXyc3BA",
        },
    }
    model_response_iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=False, json_mode=False
    )
    model_response = model_response_iterator.chunk_parser(chunk=chunk)
    print(f"\n\nmodel_response: {model_response}\n\n")
    assert model_response.choices[0].delta.thinking_blocks is not None
    assert len(model_response.choices[0].delta.thinking_blocks) == 1
    print(
        f"\n\nmodel_response.choices[0].delta.thinking_blocks[0]: {model_response.choices[0].delta.thinking_blocks[0]}\n\n"
    )
    assert (
        model_response.choices[0].delta.thinking_blocks[0]["type"]
        == "redacted_thinking"
    )

    assert model_response.choices[0].delta.provider_specific_fields is not None
    assert "thinking_blocks" in model_response.choices[0].delta.provider_specific_fields


def test_handle_json_mode_chunk_response_format_tool():
    model_response_iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=True
    )
    response_format_tool = ChatCompletionToolCallChunk(
        id="tool_123",
        type="function",
        function=ChatCompletionToolCallFunctionChunk(
            name=RESPONSE_FORMAT_TOOL_NAME,
            arguments='{"question": "What is the weather?", "answer": "It is sunny"}',
        ),
        index=0,
    )

    text, tool_use = model_response_iterator._handle_json_mode_chunk(
        "", response_format_tool
    )
    print(f"\n\nresponse_format_tool text: {text}\n\n")
    print(f"\n\nresponse_format_tool tool_use: {tool_use}\n\n")

    assert text == '{"question": "What is the weather?", "answer": "It is sunny"}'
    assert tool_use is None


def test_handle_json_mode_chunk_regular_tool():
    model_response_iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=True
    )
    regular_tool = ChatCompletionToolCallChunk(
        id="tool_456",
        type="function",
        function=ChatCompletionToolCallFunctionChunk(
            name="get_weather", arguments='{"location": "San Francisco, CA"}'
        ),
        index=0,
    )

    text, tool_use = model_response_iterator._handle_json_mode_chunk("", regular_tool)
    print(f"\n\nregular_tool text: {text}\n\n")
    print(f"\n\nregular_tool tool_use: {tool_use}\n\n")

    assert text == ""
    assert tool_use is not None
    assert tool_use["function"]["name"] == "get_weather"


def test_handle_json_mode_chunk_streaming_response_format_tool():
    model_response_iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=True
    )

    # First chunk: response_format tool with id and name, but no arguments
    first_chunk = ChatCompletionToolCallChunk(
        id="tool_123",
        type="function",
        function=ChatCompletionToolCallFunctionChunk(
            name=RESPONSE_FORMAT_TOOL_NAME, arguments=""
        ),
        index=0,
    )

    # Second chunk: continuation with arguments delta (no id)
    second_chunk = ChatCompletionToolCallChunk(
        id=None,
        type="function",
        function=ChatCompletionToolCallFunctionChunk(
            name=None, arguments='{"question": "What is the weather?"'
        ),
        index=0,
    )

    # Third chunk: more arguments delta (no id)
    third_chunk = ChatCompletionToolCallChunk(
        id=None,
        type="function",
        function=ChatCompletionToolCallFunctionChunk(
            name=None, arguments=', "answer": "It is sunny"}'
        ),
        index=0,
    )

    # Process first chunk - should set tracking flag but not convert yet (no args)
    text1, tool_use1 = model_response_iterator._handle_json_mode_chunk("", first_chunk)
    print(f"\n\nfirst_chunk text: {text1}\n\n")
    print(f"\n\nfirst_chunk tool_use: {tool_use1}\n\n")

    # Process second chunk - should convert arguments to text
    text2, tool_use2 = model_response_iterator._handle_json_mode_chunk("", second_chunk)
    print(f"\n\nsecond_chunk text: {text2}\n\n")
    print(f"\n\nsecond_chunk tool_use: {tool_use2}\n\n")

    # Process third chunk - should convert arguments to text
    text3, tool_use3 = model_response_iterator._handle_json_mode_chunk("", third_chunk)
    print(f"\n\nthird_chunk text: {text3}\n\n")
    print(f"\n\nthird_chunk tool_use: {tool_use3}\n\n")

    # Verify response_format tool chunks are converted to content
    assert text1 == ""  # First chunk has no arguments
    assert tool_use1 is None  # Tool call suppressed

    assert text2 == '{"question": "What is the weather?"'  # Second chunk arguments
    assert tool_use2 is None  # Tool call suppressed

    assert text3 == ', "answer": "It is sunny"}'  # Third chunk arguments
    assert tool_use3 is None  # Tool call suppressed


def test_handle_json_mode_chunk_streaming_regular_tool():
    model_response_iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=True
    )

    # First chunk: regular tool with id and name, but no arguments
    first_chunk = ChatCompletionToolCallChunk(
        id="tool_456",
        type="function",
        function=ChatCompletionToolCallFunctionChunk(name="get_weather", arguments=""),
        index=0,
    )

    # Second chunk: continuation with arguments delta (no id)
    second_chunk = ChatCompletionToolCallChunk(
        id=None,
        type="function",
        function=ChatCompletionToolCallFunctionChunk(
            name=None, arguments='{"location": "San Francisco, CA"}'
        ),
        index=0,
    )

    # Process first chunk - should pass through as regular tool
    text1, tool_use1 = model_response_iterator._handle_json_mode_chunk("", first_chunk)
    print(f"\n\nregular first_chunk text: {text1}\n\n")
    print(f"\n\nregular first_chunk tool_use: {tool_use1}\n\n")

    # Process second chunk - should pass through as regular tool
    text2, tool_use2 = model_response_iterator._handle_json_mode_chunk("", second_chunk)
    print(f"\n\nregular second_chunk text: {text2}\n\n")
    print(f"\n\nregular second_chunk tool_use: {tool_use2}\n\n")

    # Verify regular tool chunks are passed through unchanged
    assert text1 == ""  # Original text unchanged
    assert tool_use1 is not None  # Tool call preserved
    assert tool_use1["function"]["name"] == "get_weather"

    assert text2 == ""  # Original text unchanged
    assert tool_use2 is not None  # Tool call preserved
    assert tool_use2["function"]["arguments"] == '{"location": "San Francisco, CA"}'


def test_response_format_tool_finish_reason():
    model_response_iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=True
    )

    # First chunk: response_format tool
    response_format_tool = ChatCompletionToolCallChunk(
        id="tool_123",
        type="function",
        function=ChatCompletionToolCallFunctionChunk(
            name=RESPONSE_FORMAT_TOOL_NAME, arguments='{"answer": "test"}'
        ),
        index=0,
    )

    # Process the tool call (should set converted_response_format_tool flag)
    text, tool_use = model_response_iterator._handle_json_mode_chunk(
        "", response_format_tool
    )
    print(
        f"\n\nconverted_response_format_tool flag: {model_response_iterator.converted_response_format_tool}\n\n"
    )

    # Simulate message_delta chunk with tool_use stop_reason
    message_delta_chunk = {
        "type": "message_delta",
        "delta": {"stop_reason": "tool_use", "stop_sequence": None},
        "usage": {"output_tokens": 10},
    }

    # Process the message_delta chunk
    model_response = model_response_iterator.chunk_parser(message_delta_chunk)
    print(f"\n\nfinish_reason: {model_response.choices[0].finish_reason}\n\n")

    # Verify that finish_reason is overridden to "stop" for response_format tools
    assert model_response_iterator.converted_response_format_tool is True
    assert model_response.choices[0].finish_reason == "stop"


def test_regular_tool_finish_reason():
    model_response_iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=True
    )

    # First chunk: regular tool (not response_format)
    regular_tool = ChatCompletionToolCallChunk(
        id="tool_456",
        type="function",
        function=ChatCompletionToolCallFunctionChunk(
            name="get_weather", arguments='{"location": "San Francisco, CA"}'
        ),
        index=0,
    )

    # Process the tool call (should NOT set converted_response_format_tool flag)
    text, tool_use = model_response_iterator._handle_json_mode_chunk("", regular_tool)
    print(
        f"\n\nconverted_response_format_tool flag: {model_response_iterator.converted_response_format_tool}\n\n"
    )

    # Simulate message_delta chunk with tool_use stop_reason
    message_delta_chunk = {
        "type": "message_delta",
        "delta": {"stop_reason": "tool_use", "stop_sequence": None},
        "usage": {"output_tokens": 10},
    }

    # Process the message_delta chunk
    model_response = model_response_iterator.chunk_parser(message_delta_chunk)
    print(f"\n\nfinish_reason: {model_response.choices[0].finish_reason}\n\n")

    # Verify that finish_reason remains "tool_calls" for regular tools
    assert model_response_iterator.converted_response_format_tool is False
    assert model_response.choices[0].finish_reason == "tool_calls"


def test_text_only_streaming_has_index_zero():
    """Test that text-only streaming responses have choice index=0"""
    chunks = [
        {
            "type": "message_start",
            "message": {
                "id": "msg_123",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 10, "output_tokens": 1},
            },
        },
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {"type": "text", "text": ""},
        },
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {"type": "text_delta", "text": "Hello"},
        },
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {"type": "text_delta", "text": " world"},
        },
        {"type": "content_block_stop", "index": 0},
        {
            "type": "message_delta",
            "delta": {"stop_reason": "end_turn"},
            "usage": {"output_tokens": 2},
        },
    ]

    iterator = ModelResponseIterator(None, sync_stream=True)

    # Check all chunks have choice index=0
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        if parsed.choices:
            assert (
                parsed.choices[0].index == 0
            ), f"Expected index=0, got {parsed.choices[0].index}"


def test_streaming_thinking_deltas_count_reasoning_tokens_in_usage():
    """Anthropic streaming usage should account for emitted thinking deltas."""
    chunks = [
        {
            "type": "message_start",
            "message": {
                "id": "msg_123",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 10, "output_tokens": 1},
            },
        },
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {"type": "thinking", "thinking": ""},
        },
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {
                "type": "thinking_delta",
                "thinking": "First I need to count the favorable outcomes. ",
            },
        },
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {
                "type": "thinking_delta",
                "thinking": "Then I compare that count with all possible outcomes.",
            },
        },
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {"type": "signature_delta", "signature": "sig_123"},
        },
        {"type": "content_block_stop", "index": 0},
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {"type": "text", "text": ""},
        },
        {
            "type": "content_block_delta",
            "index": 1,
            "delta": {"type": "text_delta", "text": "The probability is 3/8."},
        },
        {"type": "content_block_stop", "index": 1},
        {
            "type": "message_delta",
            "delta": {"stop_reason": "end_turn"},
            "usage": {"output_tokens": 50},
        },
    ]

    iterator = ModelResponseIterator(None, sync_stream=True)
    final_usage = None
    reasoning_deltas = []

    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        reasoning_content = getattr(parsed.choices[0].delta, "reasoning_content", None)
        if reasoning_content:
            reasoning_deltas.append(reasoning_content)
        if parsed.usage is not None:
            final_usage = parsed.usage

    assert reasoning_deltas == [
        "First I need to count the favorable outcomes. ",
        "Then I compare that count with all possible outcomes.",
    ]
    assert final_usage is not None
    completion_tokens_details = final_usage.completion_tokens_details
    assert completion_tokens_details is not None
    assert completion_tokens_details.reasoning_tokens > 0
    assert completion_tokens_details.text_tokens == (
        final_usage.completion_tokens - completion_tokens_details.reasoning_tokens
    )


def test_anthropic_completion_streaming_usage_matches_non_streaming_with_thinking():
    """The completion API should preserve Anthropic thinking usage in streaming mode."""
    thinking_parts = [
        "First I need to count the favorable outcomes. ",
        "Then I compare that count with all possible outcomes.",
    ]
    thinking_text = "".join(thinking_parts)
    answer_text = "The probability is 3/8."
    requests_seen = []

    class MockAnthropicHandler(BaseHTTPRequestHandler):
        protocol_version = "HTTP/1.1"

        def log_message(self, format, *args):  # type: ignore[no-untyped-def]
            return

        def do_POST(self):  # type: ignore[no-untyped-def]
            content_length = int(self.headers.get("content-length", "0"))
            payload = json.loads(self.rfile.read(content_length).decode("utf-8"))
            requests_seen.append(
                {
                    "path": self.path,
                    "model": payload.get("model"),
                    "stream": payload.get("stream", False),
                    "thinking": payload.get("thinking"),
                }
            )

            if payload.get("stream"):
                events = [
                    {
                        "type": "message_start",
                        "message": {
                            "id": "msg_mock",
                            "type": "message",
                            "role": "assistant",
                            "model": payload.get("model"),
                            "content": [],
                            "usage": {"input_tokens": 10, "output_tokens": 1},
                        },
                    },
                    {
                        "type": "content_block_start",
                        "index": 0,
                        "content_block": {"type": "thinking", "thinking": ""},
                    },
                    {
                        "type": "content_block_delta",
                        "index": 0,
                        "delta": {
                            "type": "thinking_delta",
                            "thinking": thinking_parts[0],
                        },
                    },
                    {
                        "type": "content_block_delta",
                        "index": 0,
                        "delta": {
                            "type": "thinking_delta",
                            "thinking": thinking_parts[1],
                        },
                    },
                    {
                        "type": "content_block_delta",
                        "index": 0,
                        "delta": {
                            "type": "signature_delta",
                            "signature": "sig_mock",
                        },
                    },
                    {"type": "content_block_stop", "index": 0},
                    {
                        "type": "content_block_start",
                        "index": 1,
                        "content_block": {"type": "text", "text": ""},
                    },
                    {
                        "type": "content_block_delta",
                        "index": 1,
                        "delta": {"type": "text_delta", "text": answer_text},
                    },
                    {"type": "content_block_stop", "index": 1},
                    {
                        "type": "message_delta",
                        "delta": {"stop_reason": "end_turn", "stop_sequence": None},
                        "usage": {"output_tokens": 50},
                    },
                    {"type": "message_stop"},
                ]
                self._write_response(
                    content_type="text/event-stream",
                    body="".join(
                        f"data: {json.dumps(event)}\n\n" for event in events
                    ).encode("utf-8"),
                )
                return

            self._write_response(
                content_type="application/json",
                body=json.dumps(
                    {
                        "id": "msg_mock",
                        "type": "message",
                        "role": "assistant",
                        "model": payload.get("model"),
                        "content": [
                            {
                                "type": "thinking",
                                "thinking": thinking_text,
                                "signature": "sig_mock",
                            },
                            {"type": "text", "text": answer_text},
                        ],
                        "stop_reason": "end_turn",
                        "stop_sequence": None,
                        "usage": {"input_tokens": 10, "output_tokens": 50},
                    }
                ).encode("utf-8"),
            )

        def _write_response(self, content_type: str, body: bytes) -> None:
            self.send_response(200)
            self.send_header("content-type", content_type)
            self.send_header("content-length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)

    server = ThreadingHTTPServer(("127.0.0.1", 0), MockAnthropicHandler)
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()

    try:
        request_kwargs = {
            "model": "anthropic/claude-sonnet-4-6",
            "api_base": f"http://127.0.0.1:{server.server_port}",
            "api_key": "test",
            "messages": [
                {
                    "role": "user",
                    "content": "Solve a probability problem and show thinking.",
                }
            ],
            "thinking": {"type": "adaptive"},
            "max_tokens": 128,
        }

        non_stream_response = litellm.completion(**request_kwargs, stream=False)
        non_stream_details = non_stream_response.usage.completion_tokens_details
        assert non_stream_details is not None
        assert non_stream_details.reasoning_tokens > 0

        reasoning_chunks = []
        content_chunks = []
        stream_usage = None
        for chunk in litellm.completion(
            **request_kwargs,
            stream=True,
            stream_options={"include_usage": True},
        ):
            chunk_dict = chunk.model_dump(exclude_none=True)
            choices = chunk_dict.get("choices") or []
            if choices:
                delta = choices[0].get("delta") or {}
                if delta.get("reasoning_content"):
                    reasoning_chunks.append(delta["reasoning_content"])
                if delta.get("content"):
                    content_chunks.append(delta["content"])
            if chunk_dict.get("usage"):
                stream_usage = chunk_dict["usage"]

        assert reasoning_chunks == thinking_parts
        assert content_chunks == [answer_text]
        assert stream_usage is not None
        stream_completion_details = stream_usage["completion_tokens_details"]
        assert (
            stream_completion_details["reasoning_tokens"]
            == non_stream_details.reasoning_tokens
        )
        assert stream_completion_details["text_tokens"] == (
            stream_usage["completion_tokens"]
            - stream_completion_details["reasoning_tokens"]
        )
        assert requests_seen == [
            {
                "path": "/v1/messages",
                "model": "claude-sonnet-4-6",
                "stream": False,
                "thinking": {"type": "adaptive"},
            },
            {
                "path": "/v1/messages",
                "model": "claude-sonnet-4-6",
                "stream": True,
                "thinking": {"type": "adaptive"},
            },
        ]
    finally:
        server.shutdown()


def test_text_and_tool_streaming_has_index_zero():
    """Test that mixed text and tool streaming responses have choice index=0"""
    chunks = [
        {
            "type": "message_start",
            "message": {
                "id": "msg_123",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 10, "output_tokens": 1},
            },
        },
        # Reasoning content at index 0
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {"type": "text", "text": ""},
        },
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {"type": "text_delta", "text": "I need to search..."},
        },
        {"type": "content_block_stop", "index": 0},
        # Regular content at index 1
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {"type": "text", "text": ""},
        },
        {
            "type": "content_block_delta",
            "index": 1,
            "delta": {"type": "text_delta", "text": "Let me help you"},
        },
        {"type": "content_block_stop", "index": 1},
        # Tool call at index 2
        {
            "type": "content_block_start",
            "index": 2,
            "content_block": {
                "type": "tool_use",
                "id": "tool_123",
                "name": "search",
                "input": {},
            },
        },
        {
            "type": "content_block_delta",
            "index": 2,
            "delta": {"type": "input_json_delta", "partial_json": '{"query"'},
        },
        {
            "type": "content_block_delta",
            "index": 2,
            "delta": {"type": "input_json_delta", "partial_json": ': "test"}'},
        },
        {"type": "content_block_stop", "index": 2},
        {
            "type": "message_delta",
            "delta": {"stop_reason": "tool_use"},
            "usage": {"output_tokens": 10},
        },
    ]

    iterator = ModelResponseIterator(None, sync_stream=True)

    # Check all chunks have choice index=0 despite different Anthropic indices
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        if parsed.choices:
            assert (
                parsed.choices[0].index == 0
            ), f"Expected index=0 for chunk type {chunk.get('type')}, got {parsed.choices[0].index}"


def test_multiple_tools_streaming_has_index_zero():
    """Test that multiple tool calls all have choice index=0"""
    chunks = [
        {
            "type": "message_start",
            "message": {
                "id": "msg_123",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 10, "output_tokens": 1},
            },
        },
        # First tool at index 0
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {
                "type": "tool_use",
                "id": "tool_1",
                "name": "search",
                "input": {},
            },
        },
        {"type": "content_block_stop", "index": 0},
        # Second tool at index 1
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {
                "type": "tool_use",
                "id": "tool_2",
                "name": "get",
                "input": {},
            },
        },
        {"type": "content_block_stop", "index": 1},
        {
            "type": "message_delta",
            "delta": {"stop_reason": "tool_use"},
            "usage": {"output_tokens": 5},
        },
    ]

    iterator = ModelResponseIterator(None, sync_stream=True)

    # All tool chunks should have choice index=0
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        if parsed.choices:
            assert (
                parsed.choices[0].index == 0
            ), f"Expected index=0, got {parsed.choices[0].index}"


def test_streaming_chunks_have_stable_ids():
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=False, json_mode=False
    )
    first_chunk = {
        "type": "content_block_delta",
        "index": 0,
        "delta": {"type": "text_delta", "text": "Hello"},
    }
    second_chunk = {
        "type": "content_block_delta",
        "index": 0,
        "delta": {"type": "text_delta", "text": " world"},
    }

    response_one = iterator.chunk_parser(chunk=first_chunk)
    response_two = iterator.chunk_parser(chunk=second_chunk)

    assert response_one.id == response_two.id == iterator.response_id


def test_partial_json_chunk_accumulation():
    """
    Test that partial JSON chunks are accumulated correctly.

    This tests the fix for https://github.com/BerriAI/litellm/issues/17473
    where network fragmentation can cause SSE data to arrive in partial chunks.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=False
    )

    partial_chunk_1 = '{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hel'
    partial_chunk_2 = 'lo"}}'

    # First partial chunk should return None (still accumulating)
    result1 = iterator._parse_sse_data(f"data:{partial_chunk_1}")
    assert result1 is None, "First partial chunk should return None while accumulating"
    assert (
        iterator.chunk_type == "accumulated_json"
    ), "Should switch to accumulated_json mode"
    assert (
        iterator.accumulated_json == partial_chunk_1
    ), "Should have accumulated first part"

    # Second partial chunk should complete the JSON and return a parsed result
    result2 = iterator._parse_sse_data(f"data:{partial_chunk_2}")
    assert result2 is not None, "Second chunk should return parsed result"
    assert (
        iterator.accumulated_json == ""
    ), "Buffer should be cleared after successful parse"
    assert (
        result2.choices[0].delta.content == "Hello"
    ), f"Expected 'Hello', got '{result2.choices[0].delta.content}'"


def test_complete_json_chunk_no_accumulation():
    """
    Test that complete JSON chunks are parsed immediately without accumulation.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=False
    )

    complete_chunk = '{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}'

    result = iterator._parse_sse_data(f"data:{complete_chunk}")
    assert result is not None, "Complete chunk should return parsed result immediately"
    assert iterator.chunk_type == "valid_json", "Should remain in valid_json mode"
    assert iterator.accumulated_json == "", "Buffer should remain empty"
    assert (
        result.choices[0].delta.content == "Hello"
    ), f"Expected 'Hello', got '{result.choices[0].delta.content}'"


def test_multiple_partial_chunks_accumulation():
    """
    Test that multiple partial chunks can be accumulated across several iterations.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=False
    )

    # Split a JSON chunk into three parts
    part1 = '{"type":"content_block_del'
    part2 = 'ta","index":0,"delta":{"type":"text_del'
    part3 = 'ta","text":"Hello"}}'

    result1 = iterator._parse_sse_data(f"data:{part1}")
    assert result1 is None
    assert iterator.accumulated_json == part1

    result2 = iterator._parse_sse_data(f"data:{part2}")
    assert result2 is None
    assert iterator.accumulated_json == part1 + part2

    result3 = iterator._parse_sse_data(f"data:{part3}")
    assert result3 is not None
    assert iterator.accumulated_json == ""
    assert result3.choices[0].delta.content == "Hello"


def test_web_search_tool_result_no_extra_tool_calls():
    """
    Test that web_search_tool_result blocks don't emit tool call chunks.

    This tests the fix for https://github.com/BerriAI/litellm/issues/17254
    where streaming with Anthropic web search was adding trailing {} to tool call arguments.

    The issue was that web_search_tool_result blocks have input_json_delta events with {}
    that were incorrectly being converted to tool calls.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=False
    )

    # Simulate the streaming sequence:
    # 1. server_tool_use block starts (web_search)
    # 2. input_json_delta with the query
    # 3. content_block_stop
    # 4. web_search_tool_result block starts
    # 5. input_json_delta with {} (this should NOT emit a tool call)
    # 6. content_block_stop

    chunks = [
        # 1. server_tool_use block starts
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {
                "type": "server_tool_use",
                "id": "srvtoolu_01ABC123",
                "name": "web_search",
            },
        },
        # 2. input_json_delta with the query
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {"type": "input_json_delta", "partial_json": '{"query": "test"}'},
        },
        # 3. content_block_stop for server_tool_use
        {"type": "content_block_stop", "index": 0},
        # 4. web_search_tool_result block starts
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {
                "type": "web_search_tool_result",
                "tool_use_id": "srvtoolu_01ABC123",
                "content": [],
            },
        },
        # 5. input_json_delta with {} - this should NOT emit a tool call
        {
            "type": "content_block_delta",
            "index": 1,
            "delta": {"type": "input_json_delta", "partial_json": "{}"},
        },
        # 6. content_block_stop for web_search_tool_result
        {"type": "content_block_stop", "index": 1},
        # 7. Another web_search_tool_result with {} - also should NOT emit
        {
            "type": "content_block_start",
            "index": 2,
            "content_block": {
                "type": "web_search_tool_result",
                "tool_use_id": "srvtoolu_01ABC123",
                "content": [],
            },
        },
        {
            "type": "content_block_delta",
            "index": 2,
            "delta": {"type": "input_json_delta", "partial_json": "{}"},
        },
        {"type": "content_block_stop", "index": 2},
    ]

    tool_calls_emitted = []
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        if parsed.choices and parsed.choices[0].delta.tool_calls:
            for tc in parsed.choices[0].delta.tool_calls:
                tool_calls_emitted.append(tc)

    # Should have exactly 2 tool calls:
    # 1. From content_block_start (server_tool_use) with id and name
    # 2. From content_block_delta with the actual query
    assert (
        len(tool_calls_emitted) == 2
    ), f"Expected 2 tool calls, got {len(tool_calls_emitted)}"

    # First tool call should have the id and name
    assert tool_calls_emitted[0]["id"] == "srvtoolu_01ABC123"
    assert tool_calls_emitted[0]["function"]["name"] == "web_search"

    # Second tool call should have the query arguments
    assert tool_calls_emitted[1]["function"]["arguments"] == '{"query": "test"}'

    # The {} chunks from web_search_tool_result should NOT have been emitted as tool calls


def test_current_content_block_type_tracking():
    """
    Test that current_content_block_type is properly tracked and reset.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=False
    )

    # Initially should be None
    assert iterator.current_content_block_type is None

    # After server_tool_use block start
    chunk1 = {
        "type": "content_block_start",
        "index": 0,
        "content_block": {
            "type": "server_tool_use",
            "id": "srvtoolu_01ABC",
            "name": "web_search",
        },
    }
    iterator.chunk_parser(chunk1)
    assert iterator.current_content_block_type == "server_tool_use"

    # After content_block_stop
    chunk2 = {"type": "content_block_stop", "index": 0}
    iterator.chunk_parser(chunk2)
    assert iterator.current_content_block_type is None

    # After web_search_tool_result block start
    chunk3 = {
        "type": "content_block_start",
        "index": 1,
        "content_block": {
            "type": "web_search_tool_result",
            "tool_use_id": "srvtoolu_01ABC",
            "content": [],
        },
    }
    iterator.chunk_parser(chunk3)
    assert iterator.current_content_block_type == "web_search_tool_result"

    # After content_block_stop
    chunk4 = {"type": "content_block_stop", "index": 1}
    iterator.chunk_parser(chunk4)
    assert iterator.current_content_block_type is None


def test_web_search_tool_result_captured_in_provider_specific_fields():
    """
    Test that web_search_tool_result content is captured in provider_specific_fields.

    This tests the fix for https://github.com/BerriAI/litellm/issues/17737
    where streaming with Anthropic web search wasn't capturing web_search_tool_result
    blocks, causing multi-turn conversations to fail.

    The web_search_tool_result content comes ALL AT ONCE in content_block_start,
    not in deltas, so we need to capture it there.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=False
    )

    # Simulate the streaming sequence with web_search_tool_result
    chunks = [
        # 1. message_start
        {
            "type": "message_start",
            "message": {
                "id": "msg_123",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 10, "output_tokens": 1},
            },
        },
        # 2. server_tool_use block starts (web_search)
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {
                "type": "server_tool_use",
                "id": "srvtoolu_01ABC123",
                "name": "web_search",
            },
        },
        # 3. input_json_delta with the query
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {
                "type": "input_json_delta",
                "partial_json": '{"query": "otter facts"}',
            },
        },
        # 4. content_block_stop for server_tool_use
        {"type": "content_block_stop", "index": 0},
        # 5. web_search_tool_result block starts - THIS IS WHERE THE RESULTS ARE
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {
                "type": "web_search_tool_result",
                "tool_use_id": "srvtoolu_01ABC123",
                "content": [
                    {
                        "type": "web_search_result",
                        "url": "https://example.com/otters",
                        "title": "Fun Otter Facts",
                        "encrypted_content": "abc123encrypted",
                    },
                    {
                        "type": "web_search_result",
                        "url": "https://example.com/otters2",
                        "title": "More Otter Facts",
                        "encrypted_content": "def456encrypted",
                    },
                ],
            },
        },
        # 6. content_block_stop for web_search_tool_result
        {"type": "content_block_stop", "index": 1},
    ]

    web_search_results = None
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        if (
            parsed.choices
            and parsed.choices[0].delta.provider_specific_fields
            and "web_search_results" in parsed.choices[0].delta.provider_specific_fields
        ):
            web_search_results = parsed.choices[0].delta.provider_specific_fields[
                "web_search_results"
            ]

    # Verify web_search_results was captured
    assert web_search_results is not None, "web_search_results should be captured"
    assert len(web_search_results) == 1, "Should have 1 web_search_tool_result block"
    assert (
        web_search_results[0]["type"] == "web_search_tool_result"
    ), "Block type should be web_search_tool_result"
    assert (
        web_search_results[0]["tool_use_id"] == "srvtoolu_01ABC123"
    ), "tool_use_id should match"
    assert len(web_search_results[0]["content"]) == 2, "Should have 2 search results"
    assert (
        web_search_results[0]["content"][0]["title"] == "Fun Otter Facts"
    ), "First result title should match"


def test_web_fetch_tool_result_captured_in_provider_specific_fields():
    """
    Test that web_fetch_tool_result content is captured in provider_specific_fields.

    This tests the fix for https://github.com/BerriAI/litellm/issues/18137
    where streaming with Anthropic web fetch wasn't capturing web_fetch_tool_result
    blocks, causing multi-turn conversations to fail.

    The web_fetch_tool_result content comes ALL AT ONCE in content_block_start,
    not in deltas, so we need to capture it there.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=False
    )

    # Simulate the streaming sequence with web_fetch_tool_result
    chunks = [
        # 1. message_start
        {
            "type": "message_start",
            "message": {
                "id": "msg_123",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 10, "output_tokens": 1},
            },
        },
        # 2. server_tool_use block starts (web_fetch)
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {
                "type": "server_tool_use",
                "id": "srvtoolu_01ABC123",
                "name": "web_fetch",
            },
        },
        # 3. input_json_delta with the url
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {
                "type": "input_json_delta",
                "partial_json": '{"url": "https://example.com"}',
            },
        },
        # 4. content_block_stop for server_tool_use
        {"type": "content_block_stop", "index": 0},
        # 5. web_fetch_tool_result block starts - THIS IS WHERE THE RESULTS ARE
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {
                "type": "web_fetch_tool_result",
                "tool_use_id": "srvtoolu_01ABC123",
                "content": {
                    "type": "web_fetch_result",
                    "url": "https://example.com",
                    "retrieved_at": "2025-12-16T19:28:29.758000+00:00",
                    "content": {
                        "type": "document",
                        "source": {
                            "type": "text",
                            "media_type": "text/plain",
                            "data": "Hello World",
                        },
                        "title": "Example Page",
                    },
                },
            },
        },
        # 6. content_block_stop for web_fetch_tool_result
        {"type": "content_block_stop", "index": 1},
    ]

    web_search_results = None
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        if (
            parsed.choices
            and parsed.choices[0].delta.provider_specific_fields
            and "web_search_results" in parsed.choices[0].delta.provider_specific_fields
        ):
            web_search_results = parsed.choices[0].delta.provider_specific_fields[
                "web_search_results"
            ]

    # Verify web_fetch_tool_result was captured (stored in web_search_results list)
    assert web_search_results is not None, "web_search_results should be captured"
    assert len(web_search_results) == 1, "Should have 1 web_fetch_tool_result block"
    assert (
        web_search_results[0]["type"] == "web_fetch_tool_result"
    ), "Block type should be web_fetch_tool_result"
    assert (
        web_search_results[0]["tool_use_id"] == "srvtoolu_01ABC123"
    ), "tool_use_id should match"
    assert (
        web_search_results[0]["content"]["url"] == "https://example.com"
    ), "URL should match"
    assert (
        web_search_results[0]["content"]["content"]["title"] == "Example Page"
    ), "Title should match"


def test_web_fetch_tool_result_no_extra_tool_calls():
    """
    Test that web_fetch_tool_result blocks don't emit tool call chunks.

    This tests the fix for https://github.com/BerriAI/litellm/issues/18137
    where streaming with Anthropic web fetch was causing issues with tool call arguments.

    The issue was that web_fetch_tool_result blocks have input_json_delta events with {}
    that were incorrectly being converted to tool calls.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=False
    )

    # to verify it doesn't emit tool calls
    chunks = [
        # 1. web_fetch_tool_result block starts
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {
                "type": "web_fetch_tool_result",
                "tool_use_id": "srvtoolu_01ABC123",
                "content": {
                    "type": "web_fetch_result",
                    "url": "https://example.com",
                    "retrieved_at": "2025-12-16T19:28:29.758000+00:00",
                    "content": {
                        "type": "document",
                        "source": {
                            "type": "text",
                            "media_type": "text/plain",
                            "data": "Hello World",
                        },
                        "title": "Example Page",
                    },
                },
            },
        },
        # 2. input_json_delta with {} - this should NOT emit a tool call
        {
            "type": "content_block_delta",
            "index": 1,
            "delta": {"type": "input_json_delta", "partial_json": "{}"},
        },
        # 3. content_block_stop for web_fetch_tool_result
        {"type": "content_block_stop", "index": 1},
    ]

    tool_call_count = 0
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        if parsed.choices and parsed.choices[0].delta.tool_calls:
            tool_call_count += 1

    # Should have 0 tool calls - web_fetch_tool_result should not emit tool calls
    assert (
        tool_call_count == 0
    ), f"Expected 0 tool calls, got {tool_call_count}. web_fetch_tool_result should not emit tool calls"


def test_container_in_provider_specific_fields_streaming():
    """
    Test that container is captured in provider_specific_fields for streaming responses.

    When container with skills is used, the container field should be present in
    the provider_specific_fields of the message_delta chunk.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=True, json_mode=False
    )

    # Simulate streaming chunks
    chunks = [
        # 1. message_start
        {
            "type": "message_start",
            "message": {
                "id": "msg_123",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 98976, "output_tokens": 1},
            },
        },
        # 2. content_block_start for text
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {
                "type": "text",
                "text": "",
            },
        },
        # 3. content_block_delta with text
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {"type": "text_delta", "text": "Hello, this is a response"},
        },
        # 4. content_block_stop for text
        {"type": "content_block_stop", "index": 0},
        # 5. message_delta with container - THIS IS WHAT WE'RE TESTING
        {
            "type": "message_delta",
            "delta": {
                "stop_reason": "end_turn",
                "stop_sequence": None,
                "container": {
                    "id": "container_011CW9hA9zpZ8xD3bjjShy4p",
                    "expires_at": "2025-12-16T04:57:16.913181Z",
                    "skills": [
                        {
                            "type": "anthropic",
                            "skill_id": "pptx",
                            "version": "20251013",
                        }
                    ],
                },
            },
            "usage": {
                "input_tokens": 98976,
                "cache_creation_input_tokens": 0,
                "cache_read_input_tokens": 0,
                "output_tokens": 931,
                "server_tool_use": {"web_search_requests": 0},
            },
        },
    ]

    container_field = None
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        if (
            parsed.choices
            and parsed.choices[0].delta.provider_specific_fields
            and "container" in parsed.choices[0].delta.provider_specific_fields
        ):
            container_field = parsed.choices[0].delta.provider_specific_fields[
                "container"
            ]

    # Verify container was captured
    assert (
        container_field is not None
    ), "container should be captured in provider_specific_fields"
    assert (
        container_field["id"] == "container_011CW9hA9zpZ8xD3bjjShy4p"
    ), "container id should match"
    assert (
        container_field["expires_at"] == "2025-12-16T04:57:16.913181Z"
    ), "expires_at should match"
    assert len(container_field["skills"]) == 1, "Should have 1 skill"
    assert container_field["skills"][0]["skill_id"] == "pptx", "skill_id should be pptx"
    assert container_field["skills"][0]["version"] == "20251013", "version should match"


def test_container_in_provider_specific_fields_non_streaming():
    """
    Test that container is captured in provider_specific_fields for non-streaming responses.

    When container with skills is used in non-streaming, the container field should be
    present in the provider_specific_fields of the response.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=False, json_mode=False
    )

    # Simulate a message_delta chunk with container (as it would appear in non-streaming)
    message_delta_chunk = {
        "type": "message_delta",
        "delta": {
            "stop_reason": "end_turn",
            "stop_sequence": None,
            "container": {
                "id": "container_abc123xyz",
                "expires_at": "2025-12-20T10:30:00.000000Z",
                "skills": [
                    {
                        "type": "anthropic",
                        "skill_id": "code_execution",
                        "version": "latest",
                    },
                    {
                        "type": "anthropic",
                        "skill_id": "pptx",
                        "version": "20251013",
                    },
                ],
            },
        },
        "usage": {
            "input_tokens": 1000,
            "output_tokens": 200,
        },
    }

    model_response = iterator.chunk_parser(message_delta_chunk)

    # Verify container is in provider_specific_fields
    assert model_response.choices[0].delta.provider_specific_fields is not None
    assert "container" in model_response.choices[0].delta.provider_specific_fields
    container_field = model_response.choices[0].delta.provider_specific_fields[
        "container"
    ]

    assert container_field["id"] == "container_abc123xyz", "container id should match"
    assert (
        container_field["expires_at"] == "2025-12-20T10:30:00.000000Z"
    ), "expires_at should match"
    assert len(container_field["skills"]) == 2, "Should have 2 skills"
    assert (
        container_field["skills"][0]["skill_id"] == "code_execution"
    ), "First skill_id should be code_execution"
    assert (
        container_field["skills"][1]["skill_id"] == "pptx"
    ), "Second skill_id should be pptx"


def test_container_absent_when_not_provided():
    """
    Test that container is not added to provider_specific_fields when not provided.

    This ensures we don't add empty or None container fields.
    """
    iterator = ModelResponseIterator(
        streaming_response=MagicMock(), sync_stream=False, json_mode=False
    )

    # message_delta without container
    message_delta_chunk = {
        "type": "message_delta",
        "delta": {
            "stop_reason": "end_turn",
            "stop_sequence": None,
        },
        "usage": {
            "input_tokens": 1000,
            "output_tokens": 200,
        },
    }

    model_response = iterator.chunk_parser(message_delta_chunk)

    # Verify container is NOT in provider_specific_fields when not provided
    if model_response.choices[0].delta.provider_specific_fields:
        assert (
            "container" not in model_response.choices[0].delta.provider_specific_fields
        ), "container should not be present when not provided in delta"


def test_streaming_code_execution_produces_code_interpreter_results():
    """
    Test that bash_code_execution_tool_result content blocks in streaming
    produce code_interpreter_results in provider_specific_fields, so the
    Responses API layer can use them without Anthropic-specific knowledge.
    """

    chunks = [
        {
            "type": "message_start",
            "message": {
                "id": "msg_01XYZ",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 100, "output_tokens": 1},
            },
        },
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {
                "type": "text",
                "text": "",
            },
        },
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {"type": "text_delta", "text": "Running code..."},
        },
        {"type": "content_block_stop", "index": 0},
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {
                "type": "server_tool_use",
                "id": "srvtoolu_01ABC",
                "name": "bash_code_execution",
                "input": {"command": "echo hello"},
            },
        },
        {"type": "content_block_stop", "index": 1},
        {
            "type": "content_block_start",
            "index": 2,
            "content_block": {
                "type": "bash_code_execution_tool_result",
                "tool_use_id": "srvtoolu_01ABC",
                "content": {
                    "type": "bash_code_execution_result",
                    "stdout": "hello\n",
                    "stderr": "",
                    "return_code": 0,
                },
            },
        },
        {"type": "content_block_stop", "index": 2},
        {
            "type": "message_delta",
            "delta": {"stop_reason": "end_turn"},
            "usage": {"output_tokens": 50},
        },
    ]

    iterator = ModelResponseIterator(None, sync_stream=True)

    found_code_interpreter_results = False
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        psf = None
        if parsed.choices and parsed.choices[0].delta:
            psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None)
        if psf and "code_interpreter_results" in psf:
            found_code_interpreter_results = True
            results = psf["code_interpreter_results"]
            assert len(results) == 1
            assert isinstance(results[0], OutputCodeInterpreterCall)
            assert results[0].type == "code_interpreter_call"
            assert results[0].id == "srvtoolu_01ABC"
            assert results[0].code == "echo hello"
            assert results[0].outputs is not None
            assert len(results[0].outputs) == 1
            assert results[0].outputs[0].logs == "hello\n"

    assert found_code_interpreter_results, (
        "code_interpreter_results should appear in provider_specific_fields "
        "when bash_code_execution_tool_result is streamed"
    )


def test_streaming_multiple_code_executions_no_duplicates():
    """
    Test that multiple code executions in a single streaming response emit
    cumulative code_interpreter_results on each chunk (matching stream_chunk_builder's
    "last value wins" contract).  The final emission must contain ALL results.
    """
    chunks = [
        {
            "type": "message_start",
            "message": {
                "id": "msg_01XYZ",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 100, "output_tokens": 1},
            },
        },
        # First code execution
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {
                "type": "server_tool_use",
                "id": "srvtoolu_01AAA",
                "name": "bash_code_execution",
                "input": {"command": "echo first"},
            },
        },
        {"type": "content_block_stop", "index": 0},
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {
                "type": "bash_code_execution_tool_result",
                "tool_use_id": "srvtoolu_01AAA",
                "content": {
                    "type": "bash_code_execution_result",
                    "stdout": "first\n",
                    "stderr": "",
                    "return_code": 0,
                },
            },
        },
        {"type": "content_block_stop", "index": 1},
        # Second code execution
        {
            "type": "content_block_start",
            "index": 2,
            "content_block": {
                "type": "server_tool_use",
                "id": "srvtoolu_01BBB",
                "name": "bash_code_execution",
                "input": {"command": "echo second"},
            },
        },
        {"type": "content_block_stop", "index": 2},
        {
            "type": "content_block_start",
            "index": 3,
            "content_block": {
                "type": "bash_code_execution_tool_result",
                "tool_use_id": "srvtoolu_01BBB",
                "content": {
                    "type": "bash_code_execution_result",
                    "stdout": "second\n",
                    "stderr": "",
                    "return_code": 0,
                },
            },
        },
        {"type": "content_block_stop", "index": 3},
        {
            "type": "message_delta",
            "delta": {"stop_reason": "end_turn"},
            "usage": {"output_tokens": 50},
        },
    ]

    iterator = ModelResponseIterator(None, sync_stream=True)

    # Collect each emission of code_interpreter_results
    emissions = []
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        psf = None
        if parsed.choices and parsed.choices[0].delta:
            psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None)
        if psf and "code_interpreter_results" in psf:
            emissions.append(psf["code_interpreter_results"])

    # Should have 2 emissions (one per tool_result block)
    assert len(emissions) == 2, f"Expected 2 emissions, got {len(emissions)}"

    # First emission: cumulative list with 1 result
    assert len(emissions[0]) == 1
    assert emissions[0][0].id == "srvtoolu_01AAA"
    assert emissions[0][0].code == "echo first"
    assert emissions[0][0].outputs[0].logs == "first\n"

    # Second (final) emission: cumulative list with BOTH results
    # This is what stream_chunk_builder will pick as "last value wins"
    assert len(emissions[1]) == 2, (
        f"Expected final emission to have 2 results, got {len(emissions[1])}. "
        f"IDs: {[r.id for r in emissions[1]]}"
    )
    assert emissions[1][0].id == "srvtoolu_01AAA"
    assert emissions[1][0].code == "echo first"
    assert emissions[1][0].outputs[0].logs == "first\n"
    assert emissions[1][1].id == "srvtoolu_01BBB"
    assert emissions[1][1].code == "echo second"
    assert emissions[1][1].outputs[0].logs == "second\n"


def test_streaming_code_execution_input_assembled_from_deltas():
    """
    In real Anthropic streaming, content_block_start for server_tool_use has
    input: {}.  The actual input arrives via input_json_delta deltas and must
    be assembled at content_block_stop so the code field is populated.

    This test uses realistic chunk shapes (empty input in start, partial JSON
    in deltas) to exercise the input assembly path.
    """
    chunks = [
        {
            "type": "message_start",
            "message": {
                "id": "msg_01XYZ",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 100, "output_tokens": 1},
            },
        },
        # server_tool_use with empty input (real streaming behaviour)
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {
                "type": "server_tool_use",
                "id": "srvtoolu_01AAA",
                "name": "code_execution",
                "input": {},
            },
        },
        # Input arrives via deltas, split across two chunks
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {
                "type": "input_json_delta",
                "partial_json": '{"comma',
            },
        },
        {
            "type": "content_block_delta",
            "index": 0,
            "delta": {
                "type": "input_json_delta",
                "partial_json": 'nd": "echo hello"}',
            },
        },
        {"type": "content_block_stop", "index": 0},
        # Tool result
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {
                "type": "bash_code_execution_tool_result",
                "tool_use_id": "srvtoolu_01AAA",
                "content": {
                    "type": "bash_code_execution_result",
                    "stdout": "hello\n",
                    "stderr": "",
                    "return_code": 0,
                },
            },
        },
        {"type": "content_block_stop", "index": 1},
        {
            "type": "message_delta",
            "delta": {"stop_reason": "end_turn"},
            "usage": {"output_tokens": 50},
        },
    ]

    iterator = ModelResponseIterator(None, sync_stream=True)

    code_results = None
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        psf = None
        if parsed.choices and parsed.choices[0].delta:
            psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None)
        if psf and "code_interpreter_results" in psf:
            code_results = psf["code_interpreter_results"]

    # The code field must contain the assembled input, not be empty
    assert code_results is not None, "No code_interpreter_results emitted"
    assert len(code_results) == 1
    assert code_results[0].id == "srvtoolu_01AAA"
    assert code_results[0].code == "echo hello"
    assert code_results[0].outputs[0].logs == "hello\n"


def test_empty_output_produces_null_outputs():
    """
    When both stdout and stderr are empty, outputs should be None
    (matching OpenAI's native behavior) rather than [{logs: ""}].
    """
    chunks = [
        {
            "type": "message_start",
            "message": {
                "id": "msg_01XYZ",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 100, "output_tokens": 1},
            },
        },
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {
                "type": "server_tool_use",
                "id": "srvtoolu_01AAA",
                "name": "bash_code_execution",
                "input": {"command": "true"},
            },
        },
        {"type": "content_block_stop", "index": 0},
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {
                "type": "bash_code_execution_tool_result",
                "tool_use_id": "srvtoolu_01AAA",
                "content": {
                    "type": "bash_code_execution_result",
                    "stdout": "",
                    "stderr": "",
                    "return_code": 0,
                },
            },
        },
        {"type": "content_block_stop", "index": 1},
        {
            "type": "message_delta",
            "delta": {"stop_reason": "end_turn"},
            "usage": {"output_tokens": 50},
        },
    ]

    iterator = ModelResponseIterator(None, sync_stream=True)

    code_results = None
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        psf = None
        if parsed.choices and parsed.choices[0].delta:
            psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None)
        if psf and "code_interpreter_results" in psf:
            code_results = psf["code_interpreter_results"]

    assert code_results is not None, "No code_interpreter_results emitted"
    assert len(code_results) == 1
    assert code_results[0].id == "srvtoolu_01AAA"
    assert (
        code_results[0].outputs is None
    ), f"Expected outputs=None for empty execution, got {code_results[0].outputs}"


def test_non_bash_tool_result_skipped():
    """
    Tool result types other than bash_code_execution_tool_result (e.g.
    text_editor_code_execution_tool_result) should be skipped and NOT
    produce code_interpreter_call items.
    """
    chunks = [
        {
            "type": "message_start",
            "message": {
                "id": "msg_01XYZ",
                "type": "message",
                "role": "assistant",
                "content": [],
                "usage": {"input_tokens": 100, "output_tokens": 1},
            },
        },
        {
            "type": "content_block_start",
            "index": 0,
            "content_block": {
                "type": "server_tool_use",
                "id": "srvtoolu_01AAA",
                "name": "text_editor",
                "input": {"command": "view", "path": "/tmp/test.py"},
            },
        },
        {"type": "content_block_stop", "index": 0},
        # text_editor result — should NOT become a code_interpreter_call
        {
            "type": "content_block_start",
            "index": 1,
            "content_block": {
                "type": "text_editor_code_execution_tool_result",
                "tool_use_id": "srvtoolu_01AAA",
                "content": [
                    {"type": "text", "text": "file contents here"},
                ],
            },
        },
        {"type": "content_block_stop", "index": 1},
        {
            "type": "message_delta",
            "delta": {"stop_reason": "end_turn"},
            "usage": {"output_tokens": 50},
        },
    ]

    iterator = ModelResponseIterator(None, sync_stream=True)

    code_results = None
    for chunk in chunks:
        parsed = iterator.chunk_parser(chunk)
        psf = None
        if parsed.choices and parsed.choices[0].delta:
            psf = getattr(parsed.choices[0].delta, "provider_specific_fields", None)
        if psf and "code_interpreter_results" in psf:
            code_results = psf["code_interpreter_results"]

    # code_interpreter_results should be emitted but empty (no bash results)
    assert (
        code_results is not None
    ), "Expected code_interpreter_results key to be emitted"
    assert (
        len(code_results) == 0
    ), f"Expected 0 code_interpreter_results for text_editor result, got {len(code_results)}"
