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

"""Integration tests for A2A client-server interaction."""

import logging
from unittest.mock import AsyncMock

try:
  from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication
  from a2a.server.request_handlers.request_handler import RequestHandler
except ImportError:
  A2AFastAPIApplication = None
  RequestHandler = None
from a2a.types import Message as A2AMessage
from a2a.types import Part as A2APart
from a2a.types import Task
from a2a.types import TaskStatus

try:
  # 0.3.x-only wrapper part type; these integration tests are skipped on 1.x.
  from a2a.types import TextPart
except ImportError:
  TextPart = None
from google.adk.a2a import _compat
from google.adk.a2a.agent.interceptors.new_integration_extension import _NEW_A2A_ADK_INTEGRATION_EXTENSION
from google.adk.a2a.converters.to_adk_event import MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_INPUT
from google.adk.a2a.executor.config import A2aAgentExecutorConfig
from google.adk.a2a.executor.interceptors.include_artifacts_in_a2a_event import include_artifacts_in_a2a_event_interceptor
from google.adk.agents.remote_a2a_agent import A2A_METADATA_PREFIX
from google.adk.events.event import Event
from google.adk.events.event_actions import EventActions
from google.adk.platform import uuid as platform_uuid
from google.adk.runners import Runner
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.genai import types
import pytest

pytestmark = pytest.mark.skipif(
    _compat.IS_A2A_V1,
    reason="integration tests use 0.3-only A2AFastAPIApplication",
)

from .client import create_a2a_client
from .client import create_client
from .server import agent_card
from .server import create_server_app

logger = logging.getLogger("google_adk." + __name__)


def create_streaming_mock_run_async(received_requests: list):
  """Creates a mock_run_async that streams multiple chunks."""

  async def mock_run_async(**kwargs):
    received_requests.append(kwargs)
    yield Event(
        author="FakeAgent",
        content=types.Content(parts=[types.Part(text="Hello")]),
        partial=True,
    )
    yield Event(
        author="FakeAgent",
        content=types.Content(parts=[types.Part(text=" world")]),
        partial=True,
    )
    yield Event(
        author="FakeAgent",
        partial=True,
        actions=EventActions(artifact_delta={"file1": 1}),
    )
    yield Event(
        author="FakeAgent",
        content=types.Content(parts=[types.Part(text="Hello world")]),
        partial=False,
    )

  return mock_run_async


def create_non_streaming_mock_run_async(received_requests: list):
  """Creates a mock_run_async that returns a single non-streaming event."""

  async def mock_run_async(**kwargs):
    received_requests.append(kwargs)
    yield Event(
        author="FakeAgent",
        content=types.Content(parts=[types.Part(text="Hello world")]),
        partial=False,
    )

  return mock_run_async


@pytest.mark.asyncio
async def test_streaming_adk_to_streaming_a2a():
  """Test streaming of normal text chunks."""
  received_requests = []
  mock_run_async = create_streaming_mock_run_async(received_requests)

  app = create_server_app(mock_run_async)
  agent = create_client(app, streaming=True)

  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name="ClientApp", user_id="test_user", session_id="test_session"
  )
  client_runner = Runner(
      app_name="ClientApp",
      agent=agent,
      session_service=session_service,
  )

  new_message = types.Content(parts=[types.Part(text="Hi")], role="user")

  texts = []
  actions = []
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message
  ):
    if event.content and event.content.parts:
      for p in event.content.parts:
        if p.text:
          texts.append(p.text)
    if event.actions and event.actions.artifact_delta:
      actions.append(event.actions)

  assert len(received_requests) == 1
  assert received_requests[0]["session_id"] is not None

  assert texts == ["Hello", " world", "Hello world"]
  assert len(actions) == 1
  assert actions[0].artifact_delta == {"file1": 1}


@pytest.mark.asyncio
async def test_streaming_adk_to_non_streaming_a2a():
  """Test ADK streaming into A2A Non-Streaming."""
  received_requests = []
  mock_run_async = create_streaming_mock_run_async(received_requests)

  app = create_server_app(mock_run_async)
  agent = create_client(app, streaming=False)

  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name="ClientApp", user_id="test_user", session_id="test_session"
  )
  client_runner = Runner(
      app_name="ClientApp", agent=agent, session_service=session_service
  )

  new_message = types.Content(parts=[types.Part(text="Hi")], role="user")

  texts = []
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message
  ):
    if event.content and event.content.parts:
      for p in event.content.parts:
        if p.text:
          texts.append(p.text)

  assert len(received_requests) == 1
  assert texts == ["Hello world"]


@pytest.mark.asyncio
async def test_non_streaming_adk_to_streaming_a2a():
  """Test ADK Non-Streaming into A2A Streaming."""
  received_requests = []
  mock_run_async = create_non_streaming_mock_run_async(received_requests)

  app = create_server_app(mock_run_async)
  agent = create_client(app, streaming=True)

  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name="ClientApp", user_id="test_user", session_id="test_session"
  )
  client_runner = Runner(
      app_name="ClientApp", agent=agent, session_service=session_service
  )

  new_message = types.Content(parts=[types.Part(text="Hi")], role="user")

  texts = []
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message
  ):
    if event.content and event.content.parts:
      for p in event.content.parts:
        if p.text:
          texts.append(p.text)

  assert len(received_requests) == 1
  assert texts == ["Hello world"]


@pytest.mark.asyncio
async def test_non_streaming_adk_to_non_streaming_a2a():
  """Test ADK Non-Streaming into A2A Non-Streaming."""
  received_requests = []
  mock_run_async = create_non_streaming_mock_run_async(received_requests)

  app = create_server_app(mock_run_async)
  agent = create_client(app, streaming=False)

  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name="ClientApp", user_id="test_user", session_id="test_session"
  )
  client_runner = Runner(
      app_name="ClientApp", agent=agent, session_service=session_service
  )

  new_message = types.Content(parts=[types.Part(text="Hi")], role="user")

  texts = []
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message
  ):
    if event.content and event.content.parts:
      for p in event.content.parts:
        if p.text:
          texts.append(p.text)

  assert len(received_requests) == 1
  assert texts == ["Hello world"]


def create_streaming_mock_run_async_with_multiple_agents(
    received_requests: list,
):
  """Creates a mock_run_async that streams multiple chunks."""

  async def mock_run_async(**kwargs):
    received_requests.append(kwargs)
    yield Event(
        author="FakeAgent1",
        content=types.Content(parts=[types.Part(text="Hello")]),
        partial=True,
    )
    yield Event(
        author="FakeAgent2",
        content=types.Content(parts=[types.Part(text=" Hi")]),
        partial=True,
    )
    yield Event(
        author="FakeAgent1",
        content=types.Content(parts=[types.Part(text=" world")]),
        partial=True,
    )
    yield Event(
        author="FakeAgent2",
        content=types.Content(parts=[types.Part(text=" human")]),
        partial=True,
    )
    yield Event(
        author="FakeAgent1",
        content=types.Content(parts=[types.Part(text="Hello world")]),
        partial=False,
    )
    yield Event(
        author="FakeAgent2",
        content=types.Content(parts=[types.Part(text="Hi human")]),
        partial=False,
    )

  return mock_run_async


@pytest.mark.asyncio
async def test_multiple_agents_streaming_adk_to_streaming_a2a():
  """Test streaming multiple agents chunks into A2A Streaming."""
  received_requests = []
  mock_run_async = create_streaming_mock_run_async_with_multiple_agents(
      received_requests
  )

  app = create_server_app(mock_run_async)
  agent = create_client(app, streaming=True)

  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name="ClientApp", user_id="test_user", session_id="test_session"
  )
  client_runner = Runner(
      app_name="ClientApp", agent=agent, session_service=session_service
  )

  new_message = types.Content(parts=[types.Part(text="Hi")], role="user")

  texts = []
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message
  ):
    if event.content and event.content.parts:
      for p in event.content.parts:
        if p.text:
          texts.append(p.text)

  assert len(received_requests) == 1
  assert texts == [
      "Hello",
      " Hi",
      " world",
      " human",
      "Hello world",
      "Hi human",
  ]


@pytest.mark.asyncio
async def test_function_calls():
  """Test function call execution from agent."""
  received_requests = []

  async def mock_run_async(**kwargs):
    received_requests.append(kwargs)
    yield Event(
        author="FakeAgent",
        content=types.Content(
            parts=[
                types.Part(
                    function_call=types.FunctionCall(
                        name="get_weather",
                        args={"location": "San Francisco"},
                        id="call_1",
                    )
                ),
                types.Part(
                    function_response=types.FunctionResponse(
                        name="get_weather",
                        response={"temperature": "22C"},
                        id="call_1",
                    )
                ),
            ],
            role="model",
        ),
    )

  app = create_server_app(mock_run_async)
  agent = create_client(app)

  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name="ClientApp", user_id="test_user", session_id="test_session"
  )
  client_runner = Runner(
      app_name="ClientApp",
      agent=agent,
      session_service=session_service,
  )

  new_message = types.Content(parts=[types.Part(text="Hi")], role="user")

  func_calls = []
  func_responses = []
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message
  ):
    func_calls.extend(event.get_function_calls())
    if event.content and event.content.parts:
      for p in event.content.parts:
        if p.function_response:
          func_responses.append(p.function_response)

  assert len(func_calls) == 1
  assert func_calls[0].name == "get_weather"
  assert func_calls[0].args == {"location": "San Francisco"}

  assert len(func_responses) == 1
  assert func_responses[0].name == "get_weather"
  assert func_responses[0].response == {"temperature": "22C"}


def create_long_running_mock_run_async(received_requests: list):
  """Creates a mock_run_async for long running function tests."""

  async def mock_run_async(**kwargs):
    received_requests.append(kwargs)
    if len(received_requests) == 1:
      yield Event(
          author="FakeAgent",
          content=types.Content(
              parts=[
                  types.Part(
                      function_call=types.FunctionCall(
                          name="long_task", args={}, id="call_long"
                      )
                  )
              ],
              role="model",
          ),
          long_running_tool_ids={"call_long"},
      )
      yield Event(
          author="FakeAgent",
          content=types.Content(
              parts=[
                  types.Part(
                      function_response=types.FunctionResponse(
                          name="long_task",
                          response={"status": "pending"},
                          id="call_long",
                      )
                  )
              ],
              role="model",
          ),
      )
    else:
      yield Event(
          author="FakeAgent",
          content=types.Content(
              parts=[types.Part(text="Task completed well")], role="model"
          ),
      )

  return mock_run_async


@pytest.mark.asyncio
async def test_long_running_function_calls_success():
  """Test long running function calls flow success with user response."""
  received_requests = []
  mock_run_async = create_long_running_mock_run_async(received_requests)

  app = create_server_app(mock_run_async)
  agent = create_client(app, streaming=True)

  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name="ClientApp", user_id="test_user", session_id="test_session"
  )
  client_runner = Runner(
      app_name="ClientApp",
      agent=agent,
      session_service=session_service,
  )

  new_message_1 = types.Content(parts=[types.Part(text="Hi")], role="user")

  func_calls_1 = []
  func_responses_1 = []
  task_id_1 = ""
  has_long_running_id = False
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message_1
  ):
    if event.custom_metadata:
      task_id_1 = event.custom_metadata.get(
          A2A_METADATA_PREFIX + "task_id", task_id_1
      )
    if (
        event.long_running_tool_ids
        and "call_long" in event.long_running_tool_ids
    ):
      has_long_running_id = True

    func_calls_1.extend(event.get_function_calls())
    if event.content and event.content.parts:
      for p in event.content.parts:
        if p.function_response:
          func_responses_1.append(p.function_response)

  assert has_long_running_id
  assert len(func_calls_1) == 1
  assert func_calls_1[0].name == "long_task"

  assert len(func_responses_1) == 1
  assert func_responses_1[0].name == "long_task"
  assert func_responses_1[0].response == {"status": "pending"}

  new_message_2 = types.Content(
      parts=[
          types.Part(
              function_response=types.FunctionResponse(
                  name="long_task", response={"result": "done"}, id="call_long"
              )
          )
      ],
      role="user",
  )

  texts = []
  task_id_2 = ""
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message_2
  ):
    if event.custom_metadata:
      task_id_2 = event.custom_metadata.get(
          A2A_METADATA_PREFIX + "task_id", task_id_2
      )
    if event.content and event.content.parts:
      for p in event.content.parts:
        if p.text:
          texts.append(p.text)

  assert task_id_1 == task_id_2
  assert "Task completed well" in texts


@pytest.mark.asyncio
async def test_long_running_function_calls_error():
  """Test long running function calls returns error on missing response."""
  received_requests = []
  mock_run_async = create_long_running_mock_run_async(received_requests)

  app = create_server_app(mock_run_async)
  a2a_client = create_a2a_client(app, streaming=False)

  request_1 = A2AMessage(
      message_id=platform_uuid.new_uuid(),
      parts=[A2APart(root=TextPart(text="Hi"))],
      role="user",
  )
  response_1_events = []
  async for event in a2a_client.send_message(request=request_1):
    response_1_events.append(event)

  assert len(response_1_events) == 1
  # Extract task_id from Turn 1 responses
  assert response_1_events[0][1] is None
  task = response_1_events[0][0]
  assert isinstance(task, Task)
  assert task.status.state == _compat.TS_INPUT_REQUIRED
  extracted_task_id = task.id
  assert extracted_task_id is not None

  request_2 = A2AMessage(
      message_id=platform_uuid.new_uuid(),
      parts=[A2APart(root=TextPart(text="Any update?"))],
      role="user",
      task_id=extracted_task_id,
      context_id=task.context_id if hasattr(task, "context_id") else None,
  )
  response_2_events = []
  async for event in a2a_client.send_message(request=request_2):
    response_2_events.append(event)

  # Verify that we get an error response for the second request due to missing function response
  assert len(response_2_events) == 1
  assert response_2_events[0][1] is None
  error_response = response_2_events[0][0]
  assert isinstance(error_response, Task)
  assert error_response.status.message.parts[0].root.text == (
      "It was not provided a function response for the function call."
  )


@pytest.mark.asyncio
async def test_user_follow_up():
  """Test multi-turn interaction or follow up with state."""
  received_requests = []

  async def mock_run_async(**kwargs):
    received_requests.append(kwargs)
    # Yield response with custom metadata to test passing back
    yield Event(
        author="FakeAgent",
        content=types.Content(
            parts=[types.Part(text="Follow up response")], role="model"
        ),
        custom_metadata={"server_state": "active"},
    )

  app = create_server_app(mock_run_async)
  agent = create_client(app)

  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name="ClientApp", user_id="test_user", session_id="test_session"
  )
  client_runner = Runner(
      app_name="ClientApp",
      agent=agent,
      session_service=session_service,
  )

  # First Turn
  new_message_1 = types.Content(parts=[types.Part(text="Turn 1")], role="user")
  async for _ in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message_1
  ):
    pass

  # Second Turn
  new_message_2 = types.Content(parts=[types.Part(text="Turn 2")], role="user")
  last_event = None
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message_2
  ):
    last_event = event

  assert len(received_requests) == 2
  # The second request should carry the same session ID as the first
  assert (
      received_requests[1]["session_id"] == received_requests[0]["session_id"]
  )

  assert last_event is not None


@pytest.mark.asyncio
async def test_include_artifacts_in_a2a_event():
  """Test that artifacts are included in A2A events when the interceptor is enabled."""

  async def mock_run_async(**kwargs):
    yield Event(
        actions=EventActions(artifact_delta={"artifact1": 1, "artifact2": 1}),
        author="agent",
        content=types.Content(
            parts=[types.Part(text="Here are the artifacts")]
        ),
    )

  config = A2aAgentExecutorConfig(
      execute_interceptors=[include_artifacts_in_a2a_event_interceptor]
  )
  built_app = create_server_app(mock_run_async, config=config)

  a2a_client = create_a2a_client(built_app, streaming=False)

  request = A2AMessage(
      message_id="test_message_id",
      parts=[A2APart(root=TextPart(text="Hi"))],
      role="user",
  )

  events = []
  async for event in a2a_client.send_message(request=request):
    events.append(event)

  assert len(events) == 1

  task = events[0][0]
  assert isinstance(task, Task)
  assert task.artifacts is not None
  assert len(task.artifacts) == 3

  assert task.artifacts[0].parts[0].root.text == "Here are the artifacts"

  assert task.artifacts[1].artifact_id == "artifact1_1"
  assert task.artifacts[1].name == "artifact1"
  assert task.artifacts[1].parts[0].root.text == "artifact content"

  assert task.artifacts[2].artifact_id == "artifact2_1"
  assert task.artifacts[2].name == "artifact2"
  assert task.artifacts[2].parts[0].root.text == "artifact content"


@pytest.mark.asyncio
async def test_user_follow_up_sends_task_id_with_input_required():
  """Test that client follow-up sends the same task_id."""

  task_id = "mocked-task-id-123"
  context_id = "mocked-context-id-456"
  mock_task = Task(
      id=task_id,
      context_id=context_id,
      kind="task",
      status=TaskStatus(
          state=_compat.TS_INPUT_REQUIRED,
          message=A2AMessage(
              message_id="mocked-message-id-789",
              role="user",
              parts=[A2APart(root=TextPart(text="Input required"))],
          ),
      ),
      metadata={_NEW_A2A_ADK_INTEGRATION_EXTENSION: True},
  )

  mock_handler = AsyncMock(spec=RequestHandler)
  # First call returns input_required, second call completes
  mock_handler.on_message_send.side_effect = [
      mock_task,
      Task(
          id=task_id,
          context_id=context_id,
          kind="task",
          status=TaskStatus(state=_compat.TS_COMPLETED),
          metadata={_NEW_A2A_ADK_INTEGRATION_EXTENSION: True},
      ),
  ]

  app = A2AFastAPIApplication(
      agent_card=agent_card, http_handler=mock_handler
  ).build()
  agent = create_client(app, streaming=False)

  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name="ClientApp", user_id="test_user", session_id="test_session"
  )
  client_runner = Runner(
      app_name="ClientApp", agent=agent, session_service=session_service
  )

  # First Turn
  new_message_1 = types.Content(parts=[types.Part(text="Turn 1")], role="user")
  found_call_id = None
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message_1
  ):
    for call in event.get_function_calls():
      if call.name == MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_INPUT:
        found_call_id = call.id

  assert found_call_id is not None

  # Second Turn (Follow-up)
  function_response = types.FunctionResponse(
      id=found_call_id,
      name=MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_INPUT,
      response={"result": "Turn 2"},
  )
  new_message_2 = types.Content(
      parts=[types.Part(function_response=function_response)], role="user"
  )
  async for _ in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message_2
  ):
    pass

  assert mock_handler.on_message_send.call_count == 2
  # Second call args
  call_args_2 = mock_handler.on_message_send.call_args_list[1]
  params_2 = call_args_2[0][0]
  assert params_2.message.task_id == task_id


@pytest.mark.asyncio
async def test_user_follow_up_sends_task_id_with_input_required_legacy_impl():
  """Test that client follow-up sends the same task_id."""

  task_id = "mocked-task-id-123"
  context_id = "mocked-context-id-456"
  mock_task = Task(
      id=task_id,
      context_id=context_id,
      kind="task",
      status=TaskStatus(
          state=_compat.TS_INPUT_REQUIRED,
          message=A2AMessage(
              message_id="mocked-message-id-789",
              role="user",
              parts=[A2APart(root=TextPart(text="Input required"))],
          ),
      ),
  )

  mock_handler = AsyncMock(spec=RequestHandler)
  # First call returns input_required, second call completes
  mock_handler.on_message_send.side_effect = [
      mock_task,
      Task(
          id=task_id,
          context_id=context_id,
          kind="task",
          status=TaskStatus(state=_compat.TS_COMPLETED),
      ),
  ]

  app = A2AFastAPIApplication(
      agent_card=agent_card, http_handler=mock_handler
  ).build()
  agent = create_client(app, streaming=False)

  session_service = InMemorySessionService()
  await session_service.create_session(
      app_name="ClientApp", user_id="test_user", session_id="test_session"
  )
  client_runner = Runner(
      app_name="ClientApp", agent=agent, session_service=session_service
  )

  # First Turn
  new_message_1 = types.Content(parts=[types.Part(text="Turn 1")], role="user")
  found_call_id = None
  async for event in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message_1
  ):
    for call in event.get_function_calls():
      if call.name == MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_INPUT:
        found_call_id = call.id

  assert found_call_id is not None

  # Second Turn (Follow-up)
  function_response = types.FunctionResponse(
      id=found_call_id,
      name=MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_INPUT,
      response={"result": "Turn 2"},
  )
  new_message_2 = types.Content(
      parts=[types.Part(function_response=function_response)], role="user"
  )
  async for _ in client_runner.run_async(
      user_id="test_user", session_id="test_session", new_message=new_message_2
  ):
    pass

  assert mock_handler.on_message_send.call_count == 2
  # Second call args
  call_args_2 = mock_handler.on_message_send.call_args_list[1]
  params_2 = call_args_2[0][0]
  assert params_2.message.task_id == task_id
