# Copyright 2026 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.

from __future__ import annotations

from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.tools.function_tool import FunctionTool
from google.adk.tools.long_running_tool import LongRunningFunctionTool
from google.adk.tools.tool_context import ToolContext
from google.genai import types
import pytest

from tests.unittests import testing_utils
from tests.unittests.agents.llm.event_utils import text_parts

_USER_ID = 'test_user'
_SESSION_ID = 'test_session'


async def _setup_runner(mock_model, tools=None, **agent_kwargs):
  """Setup runner with LlmAgent directly."""
  llm_agent = LlmAgent(
      name='test_agent',
      model=mock_model,
      tools=tools or [],
      **agent_kwargs,
  )
  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name='test', user_id=_USER_ID, session_id=_SESSION_ID
  )
  runner = Runner(
      app_name='test',
      agent=llm_agent,
      session_service=session_service,
  )
  return runner


async def _run_turn(runner, user_message):
  """Run a single turn."""
  return [
      e
      async for e in runner.run_async(
          user_id=_USER_ID,
          session_id=_SESSION_ID,
          new_message=types.Content(
              role='user', parts=[types.Part(text=user_message)]
          ),
      )
  ]


async def _resume_turn(
    runner, prev_events, tool_name, tool_response_value='done'
):
  """Resume after an interrupt."""
  fc_ids = []
  for e in prev_events:
    if e.content and e.content.parts:
      for p in e.content.parts:
        if (
            p.function_call
            and p.function_call.name == tool_name
            and p.function_call.id
        ):
          fc_ids.append(p.function_call.id)
    if getattr(e.output, 'function_calls', None):
      for fc in e.output.function_calls:
        if fc.name == tool_name and fc.id:
          fc_ids.append(fc.id)

  if not fc_ids:
    for e in prev_events:
      if e.long_running_tool_ids:
        fc_ids = list(e.long_running_tool_ids)
        break

  invocation_id = prev_events[0].invocation_id

  fr_parts = [
      types.Part(
          function_response=types.FunctionResponse(
              name=tool_name,
              id=fc_id,
              response={'result': tool_response_value},
          )
      )
      for fc_id in fc_ids
  ]
  resume_msg = types.Content(role='user', parts=fr_parts)

  return [
      e
      async for e in runner.run_async(
          user_id=_USER_ID,
          session_id=_SESSION_ID,
          invocation_id=invocation_id,
          new_message=resume_msg,
      )
  ]


def create_lro_tool(name: str = 'long_running_op') -> LongRunningFunctionTool:
  """Creates a minimal LRO tool for testing."""

  def _impl() -> None:
    return None

  _impl.__name__ = name
  return LongRunningFunctionTool(_impl)


# ---------------------------------------------------------------------------
# Tests: Single Agent
# ---------------------------------------------------------------------------


class TestSingleAgentInterruptions:
  """Tests for single agent triggering interruptions."""

  async def test_single_agent_yields_on_long_running_tool(self):
    """Single agent yields on Long Running Tool.

    Arrange: Set up a single agent with a long running tool.
    Act: Run the agent with a prompt that triggers the tool.
    Assert: Verify that the execution yields a long running tool interrupt.
    """

    fc = types.Part.from_function_call(name='long_running_op', args={})
    mock_model = testing_utils.MockModel.create(responses=[fc, 'Final answer'])

    lro_tool = create_lro_tool()
    runner = await _setup_runner(mock_model, tools=[lro_tool])

    # Act: Run first turn
    events = await _run_turn(runner, 'Go')

    # Assert: Should have triggered function call
    assert any(
        any(
            p.function_call and p.function_call.name == 'long_running_op'
            for p in e.content.parts or []
        )
        for e in events
    )
    assert len(mock_model.requests) == 1

    # Act: Resume
    resume_events = await _resume_turn(runner, events, 'long_running_op')

    # Assert: Should have completed
    assert any('Final answer' in t for t in text_parts(resume_events))
    assert len(mock_model.requests) == 2

  async def test_single_agent_request_input_tool_interrupt_and_resume(self):
    """Test that using RequestInputTool successfully triggers an interrupt and resumes with user input."""
    from google.adk.tools import request_input

    fc = types.Part.from_function_call(
        name='adk_request_input',
        args={'message': 'Which file?', 'response_schema': {'type': 'string'}},
    )
    mock_model = testing_utils.MockModel.create(
        responses=[fc, 'Continuing with file: file_a.txt']
    )

    runner = await _setup_runner(mock_model, tools=[request_input])

    # Act: Run first turn
    events = await _run_turn(runner, 'Start')

    # Assert: Verify the interrupt event is produced
    assert any(e.long_running_tool_ids for e in events)
    assert any(
        any(
            p.function_call and p.function_call.name == 'adk_request_input'
            for p in e.content.parts or []
        )
        for e in events
    )

    # Act: Resume with the response
    resume_events = await _resume_turn(
        runner, events, 'adk_request_input', tool_response_value='file_a.txt'
    )

    # Assert: Execution should continue with user response in the prompt history
    assert len(mock_model.requests) == 2

    # Assert: Verify the second request contains the FunctionCall & FunctionResponse pair
    second_req_contents = mock_model.requests[1].contents
    assert any(
        any(
            p.function_call and p.function_call.name == 'adk_request_input'
            for p in c.parts or []
        )
        for c in second_req_contents
    )
    assert any(
        any(
            p.function_response
            and p.function_response.name == 'adk_request_input'
            for p in c.parts or []
        )
        for c in second_req_contents
    )

  async def test_single_agent_request_input_tool_structured_schema(self):
    """Test that using RequestInputTool with a structured object schema successfully interrupts and resumes with a dictionary response."""
    from google.adk.tools import request_input

    schema = {
        'type': 'object',
        'properties': {
            'host': {'type': 'string'},
            'port': {'type': 'integer'},
        },
        'required': ['host'],
    }
    fc = types.Part.from_function_call(
        name='adk_request_input',
        args={
            'message': 'Provide DB connection details:',
            'response_schema': schema,
        },
    )
    mock_model = testing_utils.MockModel.create(
        responses=[fc, 'Connected to localhost:3306']
    )

    runner = await _setup_runner(mock_model, tools=[request_input])

    # Act: Run first turn
    events = await _run_turn(runner, 'Start')

    # Assert: Verify the interrupt event is produced with the schema args
    assert any(e.long_running_tool_ids for e in events)
    fc_event = next(
        e
        for e in events
        if e.content
        and any(
            p.function_call and p.function_call.name == 'adk_request_input'
            for p in e.content.parts or []
        )
    )
    fc_part = next(p for p in fc_event.content.parts if p.function_call)
    assert fc_part.function_call.args['response_schema'] == schema

    # Act: Resume with a structured dict response
    db_details = {'host': 'localhost', 'port': 3306}
    resume_events = await _resume_turn(
        runner, events, 'adk_request_input', tool_response_value=db_details
    )

    # Assert: Execution should continue with the structured user response
    assert len(mock_model.requests) == 2

    # Assert: Verify the second request contains the FunctionCall & FunctionResponse pair
    second_req_contents = mock_model.requests[1].contents
    assert any(
        any(
            p.function_call and p.function_call.name == 'adk_request_input'
            for p in c.parts or []
        )
        for c in second_req_contents
    )
    assert any(
        any(
            p.function_response
            and p.function_response.name == 'adk_request_input'
            for p in c.parts or []
        )
        for c in second_req_contents
    )


class TestNestedAgentInterruptions:
  """Tests for multi-agent setups with interruptions."""

  async def test_child_agent_interrupt_and_resume(self):
    """Child agent yields on LRO and resumes successfully.

    Arrange: Parent agent with Child agent. Parent transfers to Child.
      Child calls LRO tool.
    Act: Run, expect LRO interrupt. Then resume.
    Assert: Should complete successfully.
    """

    def transfer_to_child(tool_context: ToolContext) -> str:
      tool_context.actions.transfer_to_agent = 'child_agent'
      return 'transferring'

    # Child agent
    fc_child = types.Part.from_function_call(name='child_lro', args={})
    child_mock_model = testing_utils.MockModel.create(
        responses=[fc_child, 'Child final answer']
    )

    lro_tool = create_lro_tool('child_lro')

    child_agent = LlmAgent(
        name='child_agent',
        model=child_mock_model,
        tools=[lro_tool],
    )

    # Parent agent
    fc_parent = types.Part.from_function_call(name='transfer_to_child', args={})
    parent_mock_model = testing_utils.MockModel.create(
        responses=[fc_parent, fc_parent, 'Parent final answer']
    )

    parent_agent = LlmAgent(
        name='parent_agent',
        model=parent_mock_model,
        tools=[transfer_to_child],
        sub_agents=[child_agent],
    )

    # Setup runner
    session_service = InMemorySessionService()
    await session_service.create_session(
        app_name='test', user_id=_USER_ID, session_id=_SESSION_ID
    )
    runner = Runner(
        app_name='test', agent=parent_agent, session_service=session_service
    )

    # When Parent runs the first turn
    events = await _run_turn(runner, 'Go')

    # Then it should trigger child LRO interrupt
    assert any(e.long_running_tool_ids for e in events)

    # When Parent resumes the turn
    resume_events = await _resume_turn(runner, events, 'child_lro')

    # Then it should complete successfully
    assert any('Child final answer' in t for t in text_parts(resume_events))

  @pytest.mark.xfail(reason='Task agent as subagent not supported yet.')
  async def test_task_child_agent_interrupt_and_resume(self):
    """Task child agent yields on LRO and resumes successfully.

    Arrange: Parent agent with Task Child agent. Parent transfers to Child.
      Child calls LRO tool.
    Act: Run, expect LRO interrupt. Then resume.
    Assert: Should complete successfully.
    """

    def transfer_to_child(tool_context: ToolContext) -> str:
      tool_context.actions.transfer_to_agent = 'child_agent'
      return 'transferring'

    # Child agent (Task mode)
    fc_child = types.Part.from_function_call(name='child_lro', args={})
    fc_finish = types.Part.from_function_call(
        name='finish_task', args={'result': 'Task done'}
    )
    child_mock_model = testing_utils.MockModel.create(
        responses=[fc_child, fc_finish, 'Child final answer']
    )

    lro_tool = create_lro_tool('child_lro')

    child_agent = LlmAgent(
        name='child_agent',
        model=child_mock_model,
        tools=[lro_tool],
        mode='task',
    )

    # Parent agent
    fc_parent = types.Part.from_function_call(name='transfer_to_child', args={})
    parent_mock_model = testing_utils.MockModel.create(
        responses=[fc_parent, 'Parent final answer']
    )

    parent_agent = LlmAgent(
        name='parent_agent',
        model=parent_mock_model,
        tools=[transfer_to_child],
        sub_agents=[child_agent],
    )

    # Setup runner
    session_service = InMemorySessionService()
    await session_service.create_session(
        app_name='test', user_id=_USER_ID, session_id=_SESSION_ID
    )
    runner = Runner(
        app_name='test', agent=parent_agent, session_service=session_service
    )

    # When Parent runs the first turn
    events = await _run_turn(runner, 'Go')

    # Then it should trigger child LRO interrupt
    assert any(e.long_running_tool_ids for e in events)

    # When Parent resumes the turn
    resume_events = await _resume_turn(runner, events, 'child_lro')

    # Then it should complete successfully
    assert any('Parent final answer' in t for t in text_parts(resume_events))

  @pytest.mark.xfail(reason='Single-turn agent as subagent not supported yet.')
  async def test_single_turn_child_agent_interrupt_and_resume(self):
    """Single-turn child agent yields on LRO and resumes successfully.

    Arrange: Parent agent with Single-turn Child agent.
      Parent calls Child via tool.
      Child calls LRO tool.
    Act: Run, expect LRO interrupt. Then resume.
    Assert: Should complete successfully.
    """

    # Child agent (Single-turn)
    fc_child = types.Part.from_function_call(name='child_lro', args={})
    child_mock_model = testing_utils.MockModel.create(
        responses=[fc_child, 'Child final answer']
    )

    lro_tool = create_lro_tool('child_lro')

    child_agent = LlmAgent(
        name='child_agent',
        model=child_mock_model,
        tools=[lro_tool],
        mode='single_turn',
    )

    # Parent agent
    fc_call_child = types.Part.from_function_call(
        name='child_agent', args={'request': 'Go to child'}
    )
    parent_mock_model = testing_utils.MockModel.create(
        responses=[fc_call_child, 'Parent final answer']
    )

    parent_agent = LlmAgent(
        name='parent_agent',
        model=parent_mock_model,
        sub_agents=[child_agent],
    )

    # Setup runner
    session_service = InMemorySessionService()
    await session_service.create_session(
        app_name='test', user_id=_USER_ID, session_id=_SESSION_ID
    )
    runner = Runner(
        app_name='test', agent=parent_agent, session_service=session_service
    )

    # When Parent runs the first turn
    events = await _run_turn(runner, 'Go')

    # Then it should trigger child LRO interrupt
    assert any(e.long_running_tool_ids for e in events)

    # When Parent resumes the turn
    resume_events = await _resume_turn(runner, events, 'child_lro')

    # Then it should complete successfully
    assert any('Parent final answer' in t for t in text_parts(resume_events))
