# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for function call/response rearrangement in contents module."""

from google.adk.agents.llm_agent import Agent
from google.adk.events.event import Event
from google.adk.flows.llm_flows import contents
from google.adk.models.llm_request import LlmRequest
from google.genai import types
import pytest

from ... import testing_utils


@pytest.mark.asyncio
async def test_basic_function_call_response_processing():
  """Test basic function call/response processing without rearrangement."""
  agent = Agent(model="gemini-2.5-flash", name="test_agent")
  llm_request = LlmRequest(model="gemini-2.5-flash")
  invocation_context = await testing_utils.create_invocation_context(
      agent=agent
  )

  function_call = types.FunctionCall(
      id="call_123", name="search_tool", args={"query": "test"}
  )
  function_response = types.FunctionResponse(
      id="call_123",
      name="search_tool",
      response={"results": ["item1", "item2"]},
  )

  events = [
      Event(
          invocation_id="inv1",
          author="user",
          content=types.UserContent("Search for test"),
      ),
      Event(
          invocation_id="inv2",
          author="test_agent",
          content=types.ModelContent([types.Part(function_call=function_call)]),
      ),
      Event(
          invocation_id="inv3",
          author="user",
          content=types.UserContent(
              [types.Part(function_response=function_response)]
          ),
      ),
  ]
  invocation_context.session.events = events

  # Process the request
  async for _ in contents.request_processor.run_async(
      invocation_context, llm_request
  ):
    pass

  # Verify no rearrangement occurred
  assert llm_request.contents == [
      types.UserContent("Search for test"),
      types.ModelContent([types.Part(function_call=function_call)]),
      types.UserContent([types.Part(function_response=function_response)]),
  ]


@pytest.mark.asyncio
async def test_rearrangement_with_intermediate_function_response():
  """Test rearrangement when intermediate function response appears after call."""
  agent = Agent(model="gemini-2.5-flash", name="test_agent")
  llm_request = LlmRequest(model="gemini-2.5-flash")
  invocation_context = await testing_utils.create_invocation_context(
      agent=agent
  )

  function_call = types.FunctionCall(
      id="long_call_123", name="long_running_tool", args={"task": "process"}
  )
  # First intermediate response
  intermediate_response = types.FunctionResponse(
      id="long_call_123",
      name="long_running_tool",
      response={"status": "processing", "progress": 50},
  )
  # Final response
  final_response = types.FunctionResponse(
      id="long_call_123",
      name="long_running_tool",
      response={"status": "completed", "result": "done"},
  )

  events = [
      Event(
          invocation_id="inv1",
          author="user",
          content=types.UserContent("Run long process"),
      ),
      # Function call
      Event(
          invocation_id="inv2",
          author="test_agent",
          content=types.ModelContent([types.Part(function_call=function_call)]),
      ),
      # Intermediate function response appears right after call
      Event(
          invocation_id="inv3",
          author="user",
          content=types.UserContent(
              [types.Part(function_response=intermediate_response)]
          ),
      ),
      # Some conversation happens
      Event(
          invocation_id="inv4",
          author="test_agent",
          content=types.ModelContent("Still processing..."),
      ),
      # Final function response (this triggers rearrangement)
      Event(
          invocation_id="inv5",
          author="user",
          content=types.UserContent(
              [types.Part(function_response=final_response)]
          ),
      ),
  ]
  invocation_context.session.events = events

  # Process the request
  async for _ in contents.request_processor.run_async(
      invocation_context, llm_request
  ):
    pass

  # Verify rearrangement: intermediate events removed, final response replaces intermediate
  assert llm_request.contents == [
      types.UserContent("Run long process"),
      types.ModelContent([types.Part(function_call=function_call)]),
      types.UserContent([types.Part(function_response=final_response)]),
  ]


@pytest.mark.asyncio
async def test_mixed_long_running_and_normal_function_calls():
  """Test rearrangement with mixed long-running and normal function calls in same event."""
  agent = Agent(model="gemini-2.5-flash", name="test_agent")
  llm_request = LlmRequest(model="gemini-2.5-flash")
  invocation_context = await testing_utils.create_invocation_context(
      agent=agent
  )

  # Two function calls: one long-running, one normal
  long_running_call = types.FunctionCall(
      id="lro_call_456", name="long_running_tool", args={"task": "analyze"}
  )
  normal_call = types.FunctionCall(
      id="normal_call_789", name="search_tool", args={"query": "test"}
  )

  # Intermediate response for long-running tool
  lro_intermediate_response = types.FunctionResponse(
      id="lro_call_456",
      name="long_running_tool",
      response={"status": "processing", "progress": 25},
  )
  # Response for normal tool (complete)
  normal_response = types.FunctionResponse(
      id="normal_call_789",
      name="search_tool",
      response={"results": ["item1", "item2"]},
  )
  # Final response for long-running tool
  lro_final_response = types.FunctionResponse(
      id="lro_call_456",
      name="long_running_tool",
      response={"status": "completed", "analysis": "done"},
  )

  events = [
      Event(
          invocation_id="inv1",
          author="user",
          content=types.UserContent("Analyze data and search for info"),
      ),
      # Both function calls in same event
      Event(
          invocation_id="inv2",
          author="test_agent",
          content=types.ModelContent([
              types.Part(function_call=long_running_call),
              types.Part(function_call=normal_call),
          ]),
      ),
      # Intermediate responses for both tools
      Event(
          invocation_id="inv3",
          author="user",
          content=types.UserContent([
              types.Part(function_response=lro_intermediate_response),
              types.Part(function_response=normal_response),
          ]),
      ),
      # Some conversation
      Event(
          invocation_id="inv4",
          author="test_agent",
          content=types.ModelContent("Analysis in progress, search completed"),
      ),
      # Final response for long-running tool (triggers rearrangement)
      Event(
          invocation_id="inv5",
          author="user",
          content=types.UserContent(
              [types.Part(function_response=lro_final_response)]
          ),
      ),
  ]
  invocation_context.session.events = events

  # Process the request
  async for _ in contents.request_processor.run_async(
      invocation_context, llm_request
  ):
    pass

  # Verify rearrangement: LRO intermediate replaced by final, normal tool preserved
  assert llm_request.contents == [
      types.UserContent("Analyze data and search for info"),
      types.ModelContent([
          types.Part(function_call=long_running_call),
          types.Part(function_call=normal_call),
      ]),
      types.UserContent([
          types.Part(function_response=lro_final_response),
          types.Part(function_response=normal_response),
      ]),
  ]


@pytest.mark.asyncio
async def test_completed_long_running_function_in_history():
  """Test that completed long-running function calls in history.

  Function call/response are properly rearranged and don't affect subsequent
  conversation.
  """
  agent = Agent(model="gemini-2.5-flash", name="test_agent")
  llm_request = LlmRequest(model="gemini-2.5-flash")
  invocation_context = await testing_utils.create_invocation_context(
      agent=agent
  )

  function_call = types.FunctionCall(
      id="history_call_123", name="long_running_tool", args={"task": "process"}
  )
  intermediate_response = types.FunctionResponse(
      id="history_call_123",
      name="long_running_tool",
      response={"status": "processing", "progress": 50},
  )
  final_response = types.FunctionResponse(
      id="history_call_123",
      name="long_running_tool",
      response={"status": "completed", "result": "done"},
  )

  events = [
      Event(
          invocation_id="inv1",
          author="user",
          content=types.UserContent("Start long process"),
      ),
      # Function call in history
      Event(
          invocation_id="inv2",
          author="test_agent",
          content=types.ModelContent([types.Part(function_call=function_call)]),
      ),
      # Intermediate response in history
      Event(
          invocation_id="inv3",
          author="user",
          content=types.UserContent(
              [types.Part(function_response=intermediate_response)]
          ),
      ),
      # Some conversation happens
      Event(
          invocation_id="inv4",
          author="test_agent",
          content=types.ModelContent("Still processing..."),
      ),
      # Final response completes the long-running function in history
      Event(
          invocation_id="inv5",
          author="user",
          content=types.UserContent(
              [types.Part(function_response=final_response)]
          ),
      ),
      # Agent acknowledges completion
      Event(
          invocation_id="inv6",
          author="test_agent",
          content=types.ModelContent("Process completed successfully!"),
      ),
      # Latest event is regular user message, not function response
      Event(
          invocation_id="inv7",
          author="user",
          content=types.UserContent("Great! What's next?"),
      ),
  ]
  invocation_context.session.events = events

  # Process the request
  async for _ in contents.request_processor.run_async(
      invocation_context, llm_request
  ):
    pass

  # Verify the long-running function in history was rearranged correctly:
  # - Intermediate response was replaced by final response
  # - Non-function events (like "Still processing...") are preserved
  # - No further rearrangement occurs since latest event is not function response
  assert llm_request.contents == [
      types.UserContent("Start long process"),
      types.ModelContent([types.Part(function_call=function_call)]),
      types.UserContent([types.Part(function_response=final_response)]),
      types.ModelContent("Still processing..."),
      types.ModelContent("Process completed successfully!"),
      types.UserContent("Great! What's next?"),
  ]


@pytest.mark.asyncio
async def test_completed_mixed_function_calls_in_history():
  """Test completed mixed long-running and normal function calls in history don't affect subsequent conversation."""
  agent = Agent(model="gemini-2.5-flash", name="test_agent")
  llm_request = LlmRequest(model="gemini-2.5-flash")
  invocation_context = await testing_utils.create_invocation_context(
      agent=agent
  )

  # Two function calls: one long-running, one normal
  long_running_call = types.FunctionCall(
      id="history_lro_123", name="long_running_tool", args={"task": "analyze"}
  )
  normal_call = types.FunctionCall(
      id="history_normal_456", name="search_tool", args={"query": "data"}
  )

  # Intermediate response for long-running tool
  lro_intermediate_response = types.FunctionResponse(
      id="history_lro_123",
      name="long_running_tool",
      response={"status": "processing", "progress": 30},
  )
  # Complete response for normal tool
  normal_response = types.FunctionResponse(
      id="history_normal_456",
      name="search_tool",
      response={"results": ["result1", "result2"]},
  )
  # Final response for long-running tool
  lro_final_response = types.FunctionResponse(
      id="history_lro_123",
      name="long_running_tool",
      response={"status": "completed", "analysis": "finished"},
  )

  events = [
      Event(
          invocation_id="inv1",
          author="user",
          content=types.UserContent("Analyze and search simultaneously"),
      ),
      # Both function calls in history
      Event(
          invocation_id="inv2",
          author="test_agent",
          content=types.ModelContent([
              types.Part(function_call=long_running_call),
              types.Part(function_call=normal_call),
          ]),
      ),
      # Intermediate responses for both tools in history
      Event(
          invocation_id="inv3",
          author="user",
          content=types.UserContent([
              types.Part(function_response=lro_intermediate_response),
              types.Part(function_response=normal_response),
          ]),
      ),
      # Some conversation in history
      Event(
          invocation_id="inv4",
          author="test_agent",
          content=types.ModelContent("Analysis continuing, search done"),
      ),
      # Final response completes the long-running function in history
      Event(
          invocation_id="inv5",
          author="user",
          content=types.UserContent(
              [types.Part(function_response=lro_final_response)]
          ),
      ),
      # Agent acknowledges completion
      Event(
          invocation_id="inv6",
          author="test_agent",
          content=types.ModelContent("Both tasks completed successfully!"),
      ),
      # Latest event is regular user message, not function response
      Event(
          invocation_id="inv7",
          author="user",
          content=types.UserContent("Perfect! What should we do next?"),
      ),
  ]
  invocation_context.session.events = events

  # Process the request
  async for _ in contents.request_processor.run_async(
      invocation_context, llm_request
  ):
    pass

  # Verify mixed functions in history were rearranged correctly:
  # - LRO intermediate was replaced by final response
  # - Normal tool response was preserved
  # - Non-function events preserved, no further rearrangement
  assert llm_request.contents == [
      types.UserContent("Analyze and search simultaneously"),
      types.ModelContent([
          types.Part(function_call=long_running_call),
          types.Part(function_call=normal_call),
      ]),
      types.UserContent([
          types.Part(function_response=lro_final_response),
          types.Part(function_response=normal_response),
      ]),
      types.ModelContent("Analysis continuing, search done"),
      types.ModelContent("Both tasks completed successfully!"),
      types.UserContent("Perfect! What should we do next?"),
  ]


@pytest.mark.asyncio
async def test_function_rearrangement_preserves_other_content():
  """Test that non-function content is preserved during rearrangement."""
  agent = Agent(model="gemini-2.5-flash", name="test_agent")
  llm_request = LlmRequest(model="gemini-2.5-flash")
  invocation_context = await testing_utils.create_invocation_context(
      agent=agent
  )

  function_call = types.FunctionCall(
      id="preserve_test", name="long_running_tool", args={"test": "value"}
  )
  intermediate_response = types.FunctionResponse(
      id="preserve_test",
      name="long_running_tool",
      response={"status": "processing"},
  )
  final_response = types.FunctionResponse(
      id="preserve_test",
      name="long_running_tool",
      response={"output": "preserved"},
  )

  events = [
      Event(
          invocation_id="inv1",
          author="user",
          content=types.UserContent("Before function call"),
      ),
      Event(
          invocation_id="inv2",
          author="test_agent",
          content=types.ModelContent([
              types.Part(text="I'll process this for you"),
              types.Part(function_call=function_call),
          ]),
      ),
      # Intermediate response with mixed content
      Event(
          invocation_id="inv3",
          author="user",
          content=types.UserContent([
              types.Part(text="Intermediate prefix"),
              types.Part(function_response=intermediate_response),
              types.Part(text="Processing..."),
          ]),
      ),
      # This should be removed during rearrangement
      Event(
          invocation_id="inv4",
          author="test_agent",
          content=types.ModelContent("Still working on it..."),
      ),
      # Final response with mixed content (triggers rearrangement)
      Event(
          invocation_id="inv5",
          author="user",
          content=types.UserContent([
              types.Part(text="Final prefix"),
              types.Part(function_response=final_response),
              types.Part(text="Final suffix"),
          ]),
      ),
  ]
  invocation_context.session.events = events

  # Process the request
  async for _ in contents.request_processor.run_async(
      invocation_context, llm_request
  ):
    pass

  # Verify non-function content is preserved during rearrangement
  # Intermediate response replaced by final, but ALL text content preserved
  assert llm_request.contents == [
      types.UserContent("Before function call"),
      types.ModelContent([
          types.Part(text="I'll process this for you"),
          types.Part(function_call=function_call),
      ]),
      types.UserContent([
          types.Part(text="Intermediate prefix"),
          types.Part(function_response=final_response),
          types.Part(text="Processing..."),
          types.Part(text="Final prefix"),
          types.Part(text="Final suffix"),
      ]),
  ]


@pytest.mark.asyncio
async def test_error_when_function_response_without_matching_call():
  """Test error when function response has no matching function call."""
  agent = Agent(model="gemini-2.5-flash", name="test_agent")
  llm_request = LlmRequest(model="gemini-2.5-flash")
  invocation_context = await testing_utils.create_invocation_context(
      agent=agent
  )

  # Function response without matching call
  orphaned_response = types.FunctionResponse(
      id="no_matching_call",
      name="orphaned_tool",
      response={"error": "no matching call"},
  )

  events = [
      Event(
          invocation_id="inv1",
          author="user",
          content=types.UserContent("Regular message"),
      ),
      # Response without any prior matching function call
      Event(
          invocation_id="inv2",
          author="user",
          content=types.UserContent(
              [types.Part(function_response=orphaned_response)]
          ),
      ),
  ]
  invocation_context.session.events = events

  # This should raise a ValueError during processing
  with pytest.raises(ValueError, match="No function call event found"):
    async for _ in contents.request_processor.run_async(
        invocation_context, llm_request
    ):
      pass
