# 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 unittest.mock import AsyncMock
from unittest.mock import MagicMock
from unittest.mock import patch

from google.adk.agents.context import Context
from google.adk.auth import auth_handler
from google.adk.auth.auth_credential import AuthCredential
from google.adk.auth.auth_credential import AuthCredentialTypes
from google.adk.auth.auth_tool import AuthConfig
from google.adk.events.ui_widget import UiWidget
from google.adk.memory.base_memory_service import SearchMemoryResponse
from google.adk.memory.memory_entry import MemoryEntry
from google.adk.tools.tool_confirmation import ToolConfirmation
from google.genai import types
from google.genai.types import Part
import pytest


@pytest.fixture
def mock_invocation_context():
  """Create a mock invocation context for testing."""
  mock_context = MagicMock()
  mock_context.invocation_id = "test-invocation-id"
  mock_context.agent.name = "test-agent-name"
  mock_context.session.state = {"key1": "value1", "key2": "value2"}
  mock_context.session.id = "test-session-id"
  mock_context.app_name = "test-app"
  mock_context.user_id = "test-user"
  mock_context.branch = "test-branch"
  mock_context.artifact_service = None
  mock_context.credential_service = None
  mock_context.memory_service = None
  return mock_context


def test_context_branch_returns_invocation_branch(mock_invocation_context):
  """Context.branch returns the branch from the underlying invocation context."""
  mock_invocation_context.branch = "test-branch"
  context = Context(invocation_context=mock_invocation_context)

  assert context.branch == "test-branch"


@pytest.fixture
def mock_artifact_service():
  """Create a mock artifact service for testing."""
  mock_service = AsyncMock()
  mock_service.list_artifact_keys.return_value = [
      "file1.txt",
      "file2.txt",
      "file3.txt",
  ]
  return mock_service


@pytest.fixture
def mock_auth_config(mocker):
  """Create a mock auth config for testing."""
  return mocker.create_autospec(AuthConfig, instance=True)


@pytest.fixture
def mock_auth_credential(mocker):
  """Create a mock auth credential for testing."""
  mock_credential = mocker.create_autospec(AuthCredential, instance=True)
  mock_credential.auth_type = AuthCredentialTypes.OAUTH2
  return mock_credential


class TestContextInitialization:
  """Test Context initialization."""

  def test_initialization_without_function_call_id(
      self, mock_invocation_context
  ):
    """Test Context initialization without function_call_id."""
    context = Context(mock_invocation_context)

    assert context._invocation_context == mock_invocation_context
    assert context._event_actions is not None
    assert context._state is not None
    assert context.function_call_id is None
    assert context.tool_confirmation is None

  def test_initialization_with_function_call_id(self, mock_invocation_context):
    """Test Context initialization with function_call_id."""
    context = Context(
        mock_invocation_context,
        function_call_id="test-function-call-id",
    )

    assert context.function_call_id == "test-function-call-id"
    assert context.tool_confirmation is None

  def test_initialization_with_tool_confirmation(self, mock_invocation_context):
    """Test Context initialization with tool_confirmation."""
    tool_confirmation = ToolConfirmation(
        hint="test hint", payload={"key": "value"}
    )
    context = Context(
        mock_invocation_context,
        function_call_id="test-function-call-id",
        tool_confirmation=tool_confirmation,
    )

    assert context.function_call_id == "test-function-call-id"
    assert context.tool_confirmation == tool_confirmation
    assert context.tool_confirmation.hint == "test hint"
    assert context.tool_confirmation.payload == {"key": "value"}

  def test_state_property(self, mock_invocation_context):
    """Test that state property returns mutable state."""
    context = Context(mock_invocation_context)

    assert context.state["key1"] == "value1"
    assert context.state["key2"] == "value2"

  def test_actions_property(self, mock_invocation_context):
    """Test that actions property returns event_actions."""
    context = Context(mock_invocation_context)

    assert context.actions is context._event_actions


class TestContextListArtifacts:
  """Test the list_artifacts method in Context."""

  async def test_list_artifacts_returns_artifact_keys(
      self, mock_invocation_context, mock_artifact_service
  ):
    """Test that list_artifacts returns the artifact keys from the service."""
    mock_invocation_context.artifact_service = mock_artifact_service
    context = Context(mock_invocation_context)

    result = await context.list_artifacts()

    assert result == ["file1.txt", "file2.txt", "file3.txt"]
    mock_artifact_service.list_artifact_keys.assert_called_once_with(
        app_name="test-app",
        user_id="test-user",
        session_id="test-session-id",
    )

  async def test_list_artifacts_raises_value_error_when_service_is_none(
      self, mock_invocation_context
  ):
    """Test that list_artifacts raises ValueError when no artifact service."""
    mock_invocation_context.artifact_service = None
    context = Context(mock_invocation_context)

    with pytest.raises(
        ValueError, match="Artifact service is not initialized."
    ):
      await context.list_artifacts()


class TestContextSaveLoadArtifact:
  """Test save_artifact and load_artifact methods in Context."""

  async def test_save_artifact(self, mock_invocation_context):
    """Test save_artifact method."""
    artifact_service = AsyncMock()
    artifact_service.save_artifact.return_value = 1
    mock_invocation_context.artifact_service = artifact_service

    context = Context(mock_invocation_context)
    test_artifact = Part.from_text(text="test content")

    version = await context.save_artifact("test_file.txt", test_artifact)

    artifact_service.save_artifact.assert_called_once_with(
        app_name="test-app",
        user_id="test-user",
        session_id="test-session-id",
        filename="test_file.txt",
        artifact=test_artifact,
        custom_metadata=None,
    )
    assert version == 1
    assert context.actions.artifact_delta["test_file.txt"] == 1

  async def test_load_artifact(self, mock_invocation_context):
    """Test load_artifact method."""
    artifact_service = AsyncMock()
    test_artifact = Part.from_text(text="test content")
    artifact_service.load_artifact.return_value = test_artifact
    mock_invocation_context.artifact_service = artifact_service

    context = Context(mock_invocation_context)

    result = await context.load_artifact("test_file.txt")

    artifact_service.load_artifact.assert_called_once_with(
        app_name="test-app",
        user_id="test-user",
        session_id="test-session-id",
        filename="test_file.txt",
        version=None,
    )
    assert result == test_artifact

  async def test_load_artifact_with_version(self, mock_invocation_context):
    """Test load_artifact method with specific version."""
    artifact_service = AsyncMock()
    test_artifact = Part.from_text(text="test content")
    artifact_service.load_artifact.return_value = test_artifact
    mock_invocation_context.artifact_service = artifact_service

    context = Context(mock_invocation_context)

    result = await context.load_artifact("test_file.txt", version=2)

    artifact_service.load_artifact.assert_called_once_with(
        app_name="test-app",
        user_id="test-user",
        session_id="test-session-id",
        filename="test_file.txt",
        version=2,
    )
    assert result == test_artifact


class TestContextCredentialMethods:
  """Test credential methods in Context."""

  async def test_save_credential_with_service(
      self, mock_invocation_context, mock_auth_config
  ):
    """Test save_credential when credential service is available."""
    credential_service = AsyncMock()
    mock_invocation_context.credential_service = credential_service

    context = Context(mock_invocation_context)
    await context.save_credential(mock_auth_config)

    credential_service.save_credential.assert_called_once_with(
        mock_auth_config, context
    )

  async def test_save_credential_no_service(
      self, mock_invocation_context, mock_auth_config
  ):
    """Test save_credential when credential service is not available."""
    mock_invocation_context.credential_service = None

    context = Context(mock_invocation_context)

    with pytest.raises(
        ValueError, match="Credential service is not initialized"
    ):
      await context.save_credential(mock_auth_config)

  async def test_load_credential_with_service(
      self, mock_invocation_context, mock_auth_config, mock_auth_credential
  ):
    """Test load_credential when credential service is available."""
    credential_service = AsyncMock()
    credential_service.load_credential.return_value = mock_auth_credential
    mock_invocation_context.credential_service = credential_service

    context = Context(mock_invocation_context)
    result = await context.load_credential(mock_auth_config)

    credential_service.load_credential.assert_called_once_with(
        mock_auth_config, context
    )
    assert result == mock_auth_credential

  async def test_load_credential_no_service(
      self, mock_invocation_context, mock_auth_config
  ):
    """Test load_credential when credential service is not available."""
    mock_invocation_context.credential_service = None

    context = Context(mock_invocation_context)

    with pytest.raises(
        ValueError, match="Credential service is not initialized"
    ):
      await context.load_credential(mock_auth_config)


class TestContextGetAuthResponse:
  """Test get_auth_response method in Context."""

  def test_get_auth_response(self, mock_invocation_context, mock_auth_config):
    """Test get_auth_response method."""
    context = Context(mock_invocation_context)

    with patch.object(
        auth_handler, "AuthHandler", autospec=True
    ) as mock_auth_handler:
      mock_handler_instance = mock_auth_handler.return_value
      mock_handler_instance.get_auth_response.return_value = "auth-response"

      result = context.get_auth_response(mock_auth_config)

      mock_auth_handler.assert_called_once_with(mock_auth_config)
      mock_handler_instance.get_auth_response.assert_called_once_with(
          context.state
      )
      assert result == "auth-response"


class TestContextRequestCredential:
  """Test request_credential method in Context."""

  def test_request_credential_with_function_call_id(
      self, mock_invocation_context, mock_auth_config
  ):
    """Test request_credential when function_call_id is set."""
    context = Context(
        mock_invocation_context,
        function_call_id="test-function-call-id",
    )

    with patch.object(
        auth_handler, "AuthHandler", autospec=True
    ) as mock_auth_handler:
      mock_handler_instance = mock_auth_handler.return_value
      mock_handler_instance.generate_auth_request.return_value = "auth-request"

      context.request_credential(mock_auth_config)

      mock_auth_handler.assert_called_once_with(mock_auth_config)
      mock_handler_instance.generate_auth_request.assert_called_once()
      assert (
          context.actions.requested_auth_configs["test-function-call-id"]
          == "auth-request"
      )

  def test_request_credential_without_function_call_id_raises(
      self, mock_invocation_context, mock_auth_config
  ):
    """Test request_credential raises ValueError when no function_call_id."""
    context = Context(mock_invocation_context)

    with pytest.raises(
        ValueError,
        match="request_credential requires function_call_id",
    ):
      context.request_credential(mock_auth_config)


class TestContextRequestConfirmation:
  """Test request_confirmation method in Context."""

  def test_request_confirmation_with_function_call_id(
      self, mock_invocation_context
  ):
    """Test request_confirmation when function_call_id is set."""
    context = Context(
        mock_invocation_context,
        function_call_id="test-function-call-id",
    )

    context.request_confirmation(
        hint="Please confirm", payload={"action": "delete"}
    )

    confirmation = context.actions.requested_tool_confirmations[
        "test-function-call-id"
    ]
    assert confirmation.hint == "Please confirm"
    assert confirmation.payload == {"action": "delete"}

  def test_request_confirmation_with_only_hint(self, mock_invocation_context):
    """Test request_confirmation with only hint provided."""
    context = Context(
        mock_invocation_context,
        function_call_id="test-function-call-id",
    )

    context.request_confirmation(hint="Confirm this action")

    confirmation = context.actions.requested_tool_confirmations[
        "test-function-call-id"
    ]
    assert confirmation.hint == "Confirm this action"
    assert confirmation.payload is None

  def test_request_confirmation_without_function_call_id_raises(
      self, mock_invocation_context
  ):
    """Test request_confirmation raises ValueError when no function_call_id."""
    context = Context(mock_invocation_context)

    with pytest.raises(
        ValueError,
        match="request_confirmation requires function_call_id",
    ):
      context.request_confirmation()


class TestContextMemoryMethods:
  """Test memory methods in Context."""

  async def test_add_session_to_memory_success(self, mock_invocation_context):
    """Test that add_session_to_memory calls the memory service correctly."""
    memory_service = AsyncMock()
    mock_invocation_context.memory_service = memory_service

    context = Context(mock_invocation_context)
    await context.add_session_to_memory()

    memory_service.add_session_to_memory.assert_called_once_with(
        mock_invocation_context.session
    )

  async def test_add_session_to_memory_no_service_raises(
      self, mock_invocation_context
  ):
    """Test that add_session_to_memory raises ValueError when memory service is None."""
    mock_invocation_context.memory_service = None

    context = Context(mock_invocation_context)

    with pytest.raises(
        ValueError,
        match=(
            r"Cannot add session to memory: memory service is not available\."
        ),
    ):
      await context.add_session_to_memory()

  async def test_search_memory_success(self, mock_invocation_context, mocker):
    """Test that search_memory calls the memory service correctly."""
    memory_service = AsyncMock()
    mock_search_response = mocker.create_autospec(
        SearchMemoryResponse, instance=True
    )
    memory_service.search_memory.return_value = mock_search_response
    mock_invocation_context.memory_service = memory_service

    context = Context(mock_invocation_context)
    result = await context.search_memory("test query")

    memory_service.search_memory.assert_called_once_with(
        app_name="test-app",
        user_id="test-user",
        query="test query",
    )
    assert result == mock_search_response

  async def test_search_memory_no_service_raises(self, mock_invocation_context):
    """Test that search_memory raises ValueError when memory service is None."""
    mock_invocation_context.memory_service = None

    context = Context(mock_invocation_context)

    with pytest.raises(ValueError, match="Memory service is not available."):
      await context.search_memory("test query")

  async def test_add_events_to_memory_success(self, mock_invocation_context):
    """Test that add_events_to_memory calls the memory service correctly."""
    memory_service = AsyncMock()
    mock_invocation_context.memory_service = memory_service
    test_event = MagicMock()

    context = Context(mock_invocation_context)
    await context.add_events_to_memory(
        events=[test_event],
        custom_metadata={"ttl": "6000s"},
    )

    memory_service.add_events_to_memory.assert_called_once_with(
        app_name=mock_invocation_context.session.app_name,
        user_id=mock_invocation_context.session.user_id,
        session_id=mock_invocation_context.session.id,
        events=[test_event],
        custom_metadata={"ttl": "6000s"},
    )

  async def test_add_events_to_memory_no_service_raises(
      self, mock_invocation_context
  ):
    """Test that add_events_to_memory raises ValueError when no service."""
    mock_invocation_context.memory_service = None

    context = Context(mock_invocation_context)

    with pytest.raises(
        ValueError,
        match=r"Cannot add events to memory: memory service is not available\.",
    ):
      await context.add_events_to_memory(events=[MagicMock()])

  @pytest.mark.asyncio
  async def test_add_memory_forwards_metadata(self, mock_invocation_context):
    """Tests that add_memory forwards memories and metadata."""
    memory_service = AsyncMock()
    mock_invocation_context.memory_service = memory_service
    memories = [
        MemoryEntry(content=types.Content(parts=[types.Part(text="fact one")]))
    ]
    metadata = {"ttl": "6000s"}

    context = Context(mock_invocation_context)
    await context.add_memory(memories=memories, custom_metadata=metadata)

    memory_service.add_memory.assert_called_once_with(
        app_name=mock_invocation_context.session.app_name,
        user_id=mock_invocation_context.session.user_id,
        memories=memories,
        custom_metadata=metadata,
    )

  @pytest.mark.asyncio
  async def test_add_memory_accepts_memory_entries(
      self, mock_invocation_context
  ):
    """Tests that add_memory forwards MemoryEntry inputs unchanged."""
    memory_service = AsyncMock()
    mock_invocation_context.memory_service = memory_service
    memory_entry = MemoryEntry(
        content=types.Content(parts=[types.Part(text="fact one")])
    )

    context = Context(mock_invocation_context)
    await context.add_memory(memories=[memory_entry])

    memory_service.add_memory.assert_called_once_with(
        app_name=mock_invocation_context.session.app_name,
        user_id=mock_invocation_context.session.user_id,
        memories=[memory_entry],
        custom_metadata=None,
    )

  async def test_add_memory_no_service_raises(self, mock_invocation_context):
    """Test that add_memory raises ValueError when no service."""
    mock_invocation_context.memory_service = None

    context = Context(mock_invocation_context)

    with pytest.raises(
        ValueError,
        match=r"Cannot add memory: memory service is not available\.",
    ):
      await context.add_memory(
          memories=[
              MemoryEntry(
                  content=types.Content(parts=[types.Part(text="fact one")])
              )
          ]
      )


class TestContextAddUiWidget:
  """Test render_ui_widget method in Context."""

  def test_render_ui_widget(self, mock_invocation_context):
    """Test that render_ui_widget appends a widget to actions."""

    context = Context(mock_invocation_context)
    widget = UiWidget(
        id="w1",
        provider="mcp",
        payload={"resource_uri": "ui://test-app"},
    )

    context.render_ui_widget(widget)

    assert context.actions.render_ui_widgets is not None
    assert len(context.actions.render_ui_widgets) == 1
    assert context.actions.render_ui_widgets[0] is widget

  def test_render_ui_widget_multiple(self, mock_invocation_context):
    """Test that calling render_ui_widget twice yields two widgets."""

    context = Context(mock_invocation_context)
    w1 = UiWidget(
        id="w1",
        provider="mcp",
        payload={"resource_uri": "ui://app-1"},
    )
    w2 = UiWidget(
        id="w2",
        provider="mcp",
        payload={"resource_uri": "ui://app-2"},
    )

    context.render_ui_widget(w1)
    context.render_ui_widget(w2)

    assert len(context.actions.render_ui_widgets) == 2
    assert context.actions.render_ui_widgets[0] is w1
    assert context.actions.render_ui_widgets[1] is w2

  def test_render_ui_widget_duplicate(self, mock_invocation_context):
    """Test that duplicate widgets by id are not added."""

    context = Context(mock_invocation_context)
    w1 = UiWidget(
        id="w1",
        provider="mcp",
        payload={"resource_uri": "ui://app-1"},
    )
    w2 = UiWidget(
        id="w1",
        provider="mcp",
        payload={"resource_uri": "ui://app-1-mod"},
    )

    context.render_ui_widget(w1)

    with pytest.raises(
        ValueError,
        match=(
            f"UI widget with ID '{w1.id}' already exists in the current event"
            " actions."
        ),
    ):
      context.render_ui_widget(w2)

    assert len(context.actions.render_ui_widgets) == 1
    assert context.actions.render_ui_widgets[0] is w1


class TestDeriveScheduler:
  """Tests for _derive_scheduler helper."""

  def test_derive_scheduler_no_parent(self):
    from google.adk.agents.context import _derive_scheduler

    assert _derive_scheduler(None) is None

  def test_derive_scheduler_with_parent_having_scheduler(self):
    from google.adk.agents.context import _derive_scheduler

    mock_parent = MagicMock()
    mock_scheduler = MagicMock()
    mock_parent._workflow_scheduler = mock_scheduler

    assert _derive_scheduler(mock_parent) is mock_scheduler

  def test_derive_scheduler_with_parent_no_scheduler(self):
    from google.adk.agents.context import _derive_scheduler
    from google.adk.workflow._dynamic_node_scheduler import DynamicNodeScheduler

    mock_parent = MagicMock()
    mock_parent._workflow_scheduler = None

    scheduler = _derive_scheduler(mock_parent)
    assert isinstance(scheduler, DynamicNodeScheduler)


class TestContextGetInvocationContext:
  """Test get_invocation_context method in Context."""

  def test_get_invocation_context_propagates_isolation_scope(
      self, mock_invocation_context
  ):
    """Test that get_invocation_context propagates isolation_scope to the copy."""
    context = Context(mock_invocation_context)
    context.isolation_scope = "test-isolation-scope"

    # Mock model_copy to return a mock copy
    mock_copy = MagicMock()
    mock_invocation_context.model_copy.return_value = mock_copy

    result = context.get_invocation_context()

    # Verify model_copy was called with correct update dict
    mock_invocation_context.model_copy.assert_called_once_with(
        update={
            "session": context.session,
            "isolation_scope": "test-isolation-scope",
        }
    )
    assert result is mock_copy


class TestContextRunNodeInternal:
  """Tests for the internal Context._run_node_internal orchestration method."""

  @pytest.mark.asyncio
  async def test_run_node_internal_returns_ctx_and_handles_resume_inputs(
      self, mock_invocation_context, mocker
  ):
    """Test that _run_node_internal correctly handles return_ctx and resume_inputs."""
    # Arrange
    from google.adk.agents.llm_agent import LlmAgent
    from google.adk.events.event_actions import EventActions

    agent_a = LlmAgent(name="agent_a", rerun_on_resume=True)
    root = LlmAgent(name="root", sub_agents=[agent_a], rerun_on_resume=True)
    agent_a.parent_agent = root

    root_ctx = Context(mock_invocation_context, node=root, run_id="1")

    child_ctx_a = Context(
        mock_invocation_context,
        parent_ctx=root_ctx,
        node=agent_a,
        run_id="1",
        event_actions=EventActions(),
    )
    child_ctx_a.output = "a_output"

    # Mock the standalone execution boundary
    mock_run_standalone = mocker.patch.object(
        Context,
        "_run_node_standalone",
        return_value=child_ctx_a,
    )

    # Act 1: Call _run_node_internal with return_ctx=True
    result_ctx = await root_ctx._run_node_internal(
        agent_a,
        node_input="a_input",
        return_ctx=True,
        resume_inputs={"some_key": "some_val"},
    )

    # Assert 1: It returns the child context object itself, not the output!
    assert result_ctx is child_ctx_a
    assert result_ctx.output == "a_output"

    # Assert 2: resume_inputs was correctly passed to _run_node_standalone
    mock_run_standalone.assert_called_once()
    _, kwargs = mock_run_standalone.call_args
    assert kwargs.get("resume_inputs") == {"some_key": "some_val"}


class TestContextRunNodeTransferLoop:
  """Tests for Context.run_node transfer loop orchestration."""

  @pytest.mark.asyncio
  async def test_sibling_transfer_executes_target_agent(
      self, mock_invocation_context
  ):
    """Sibling transfer routes execution to the target agent under the same parent context."""
    # Arrange
    from google.adk.agents.llm_agent import LlmAgent
    from google.adk.events.event_actions import EventActions

    # Create sibling agents under a root agent
    agent_b = LlmAgent(name="agent_b", rerun_on_resume=True)
    agent_a = LlmAgent(name="agent_a", rerun_on_resume=True)
    root = LlmAgent(
        name="root", sub_agents=[agent_a, agent_b], rerun_on_resume=True
    )
    agent_a.parent_agent = root
    agent_b.parent_agent = root

    # Create root context
    root_ctx = Context(mock_invocation_context, node=root, run_id="1")
    root_ctx._child_run_counters = {}

    # Mock child contexts returned by the scheduler
    child_ctx_a = Context(
        mock_invocation_context,
        parent_ctx=root_ctx,
        node=agent_a,
        run_id="1",
        event_actions=EventActions(transfer_to_agent="agent_b"),
    )

    child_ctx_b = Context(
        mock_invocation_context,
        parent_ctx=root_ctx,
        node=agent_b,
        run_id="1",
        event_actions=EventActions(),
    )
    child_ctx_b.output = "b_output"

    # Keep a strong reference to mock_scheduler so it doesn't get garbage
    # collected immediately (Context._workflow_scheduler uses weakref internally).
    mock_scheduler = AsyncMock(side_effect=[child_ctx_a, child_ctx_b])
    root_ctx._workflow_scheduler = mock_scheduler

    # Act
    result = await root_ctx.run_node(agent_a, node_input="a_input")

    # Assert
    assert result == "b_output"
    assert root_ctx._workflow_scheduler.call_count == 2

    # First call was agent_a
    args1, kwargs1 = root_ctx._workflow_scheduler.call_args_list[0]
    assert args1[0] is root_ctx  # parent_ctx
    assert args1[1].name == agent_a.name  # node (cloned, so compare name)
    assert args1[2] == "a_input"  # node_input
    assert kwargs1.get("run_id") == "1"

    # Second call was agent_b (transferred sibling)
    args2, kwargs2 = root_ctx._workflow_scheduler.call_args_list[1]
    assert args2[0] is root_ctx  # parent_ctx (same sibling parent!)
    assert args2[1].name == agent_b.name  # node (cloned, so compare name)
    assert args2[2] is None  # transfer input is empty
    assert kwargs2.get("run_id") == "1"  # independent counter, so still 1

  @pytest.mark.asyncio
  async def test_parent_transfer_routes_execution_to_parent_agent(
      self, mock_invocation_context
  ):
    """Parent transfer routes execution to the parent agent, climbing up the context tree."""
    # Arrange
    from google.adk.agents.llm_agent import LlmAgent
    from google.adk.events.event_actions import EventActions

    # Create hierarchical agent tree
    child = LlmAgent(name="child", rerun_on_resume=True)
    parent = LlmAgent(name="parent", sub_agents=[child], rerun_on_resume=True)
    root = LlmAgent(name="root", sub_agents=[parent], rerun_on_resume=True)
    child.parent_agent = parent
    parent.parent_agent = root

    # Create root and parent contexts
    root_ctx = Context(mock_invocation_context, node=root, run_id="1")
    root_ctx._child_run_counters = {"parent": 1}

    parent_ctx = Context(
        mock_invocation_context, parent_ctx=root_ctx, node=parent, run_id="1"
    )
    parent_ctx._child_run_counters = {"child": 1}

    # Set up scheduler on both contexts
    mock_scheduler = AsyncMock()
    root_ctx._workflow_scheduler = mock_scheduler
    parent_ctx._workflow_scheduler = mock_scheduler

    # Mock child contexts returned by the scheduler
    child_ctx = Context(
        mock_invocation_context,
        parent_ctx=parent_ctx,
        node=child,
        run_id="1",
        event_actions=EventActions(transfer_to_agent="parent"),
    )

    parent_ctx2 = Context(
        mock_invocation_context,
        parent_ctx=root_ctx,
        node=parent,
        run_id="2",
        event_actions=EventActions(),
    )
    parent_ctx2.output = "parent_output"

    mock_scheduler.side_effect = [child_ctx, parent_ctx2]

    # Act
    # We run 'child' with use_as_output=True to test flag resetting
    result = await parent_ctx.run_node(
        child, node_input="child_input", use_as_output=True
    )

    # Assert
    assert result == "parent_output"
    assert mock_scheduler.call_count == 2

    # First call: child under parent_ctx
    args1, kwargs1 = mock_scheduler.call_args_list[0]
    assert args1[0] is parent_ctx
    assert args1[1].name == child.name  # node (cloned, so compare name)
    assert kwargs1.get("use_as_output") is True

    # Second call: parent under root_ctx (climbed up!)
    args2, kwargs2 = mock_scheduler.call_args_list[1]
    assert args2[0] is root_ctx  # Climbed up to root_ctx!
    assert args2[1].name == parent.name  # node (cloned, so compare name)
    # Sibling delegation flag must be reset because we crossed parent contexts!
    assert kwargs2.get("use_as_output") is False

  @pytest.mark.asyncio
  async def test_standalone_sibling_transfer_executes_target_agent(
      self, mock_invocation_context, mocker
  ):
    """Standalone mode sibling transfer routes execution to the target agent."""
    # Arrange
    from google.adk.agents.llm_agent import LlmAgent
    from google.adk.events.event_actions import EventActions

    # Create sibling agents under a root agent
    agent_b = LlmAgent(name="agent_b", rerun_on_resume=True)
    agent_a = LlmAgent(name="agent_a", rerun_on_resume=True)
    root = LlmAgent(
        name="root", sub_agents=[agent_a, agent_b], rerun_on_resume=True
    )
    agent_a.parent_agent = root
    agent_b.parent_agent = root

    # Create root context (Mode 2: _workflow_scheduler is None)
    root_ctx = Context(mock_invocation_context, node=root, run_id="1")
    root_ctx._child_run_counters = {}
    root_ctx._workflow_scheduler = None

    # Mock child contexts returned by the standalone runner
    child_ctx_a = Context(
        mock_invocation_context,
        parent_ctx=root_ctx,
        node=agent_a,
        run_id="1",
        event_actions=EventActions(transfer_to_agent="agent_b"),
    )

    child_ctx_b = Context(
        mock_invocation_context,
        parent_ctx=root_ctx,
        node=agent_b,
        run_id="1",
        event_actions=EventActions(),
    )
    child_ctx_b.output = "standalone_b_output"

    # Mock the standalone execution boundary on the class
    mock_run_standalone = mocker.patch.object(
        Context,
        "_run_node_standalone",
        side_effect=[child_ctx_a, child_ctx_b],
    )

    # Act
    result = await root_ctx.run_node(agent_a, node_input="a_input")

    # Assert
    assert result == "standalone_b_output"
    assert mock_run_standalone.call_count == 2

    # First call was agent_a
    args1, kwargs1 = mock_run_standalone.call_args_list[0]
    assert args1[0].name == agent_a.name
    assert args1[1] == "a_input"
    assert kwargs1.get("run_id") is None

    # Second call was agent_b
    args2, kwargs2 = mock_run_standalone.call_args_list[1]
    assert args2[0].name == agent_b.name
    assert args2[1] is None
    assert kwargs2.get("run_id") is None

  @pytest.mark.asyncio
  async def test_child_transfer_routes_execution_to_child_agent(
      self, mock_invocation_context
  ):
    """Child transfer routes execution to a sub-agent (downward in hierarchy)."""
    # Arrange
    from google.adk.agents.llm_agent import LlmAgent
    from google.adk.events.event_actions import EventActions

    # Create parent-child agents
    child = LlmAgent(name="child", rerun_on_resume=True)
    parent = LlmAgent(name="parent", sub_agents=[child], rerun_on_resume=True)
    child.parent_agent = parent

    # Create parent context (the starting context of the run)
    parent_ctx = Context(mock_invocation_context, node=None, parent_ctx=None)
    mock_scheduler = AsyncMock()
    parent_ctx._workflow_scheduler = mock_scheduler

    # Mock child contexts returned by the scheduler
    # 1. Parent runs, and decides to transfer to 'child'
    parent_run_ctx = Context(
        mock_invocation_context,
        parent_ctx=parent_ctx,  # Parent is parent_ctx!
        node=parent,
        run_id="1",
        event_actions=EventActions(transfer_to_agent="child"),
    )

    # 2. Child runs successfully
    child_run_ctx = Context(
        mock_invocation_context,
        parent_ctx=parent_run_ctx,  # Parent is parent_run_ctx (inherits scheduler!)
        node=child,
        run_id="1",
        event_actions=EventActions(),
    )
    child_run_ctx.output = "child_output"

    mock_scheduler.side_effect = [parent_run_ctx, child_run_ctx]

    # Act
    result = await parent_ctx.run_node(parent, node_input="parent_input")

    # Assert
    assert result == "child_output"
    assert mock_scheduler.call_count == 2

    # First call: parent node
    args1, kwargs1 = mock_scheduler.call_args_list[0]
    assert args1[0] is parent_ctx
    assert args1[1].name == parent.name

    # Second call: child node (run under parent's execution context!)
    args2, kwargs2 = mock_scheduler.call_args_list[1]
    assert args2[0] is parent_run_ctx  # Run under parent_run_ctx!
    assert args2[1].name == child.name
    assert kwargs2.get("run_id") == "1"

  @pytest.mark.asyncio
  async def test_three_layer_transfer_round_trip(self, mock_invocation_context):
    """Verify 3-layer round trip transfer (Root -> Child -> Grandchild -> Child -> Root)."""
    # Arrange
    from google.adk.agents.llm_agent import LlmAgent
    from google.adk.events.event_actions import EventActions

    # Create 3-layer agent tree
    grandchild = LlmAgent(name="grandchild", rerun_on_resume=True)
    child = LlmAgent(
        name="child", sub_agents=[grandchild], rerun_on_resume=True
    )
    root = LlmAgent(name="root", sub_agents=[child], rerun_on_resume=True)
    grandchild.parent_agent = child
    child.parent_agent = root

    # Create root context
    root_ctx = Context(mock_invocation_context, node=None, parent_ctx=None)
    mock_scheduler = AsyncMock()
    root_ctx._workflow_scheduler = mock_scheduler

    # Step 1: Root runs (child of root_ctx)
    root_run_ctx = Context(
        mock_invocation_context,
        parent_ctx=root_ctx,  # Parent is root_ctx!
        node=root,
        run_id="1",
        event_actions=EventActions(transfer_to_agent="child"),
    )

    # Step 2: Child runs (child of root_run_ctx)
    child_run_ctx = Context(
        mock_invocation_context,
        parent_ctx=root_run_ctx,  # Parent is root_run_ctx!
        node=child,
        run_id="1",
        event_actions=EventActions(transfer_to_agent="grandchild"),
    )

    # Step 3: Grandchild runs (child of child_run_ctx)
    grandchild_run_ctx = Context(
        mock_invocation_context,
        parent_ctx=child_run_ctx,  # Parent is child_run_ctx!
        node=grandchild,
        run_id="1",
        event_actions=EventActions(transfer_to_agent="child"),
    )

    # Step 4: Child runs again (sibling of child_run_ctx, parent is root_run_ctx!)
    child_run_ctx2 = Context(
        mock_invocation_context,
        parent_ctx=root_run_ctx,  # Parent is root_run_ctx!
        node=child,
        run_id="2",
        event_actions=EventActions(transfer_to_agent="root"),
    )

    # Step 5: Root runs again (sibling of root_run_ctx, parent is root_ctx!)
    root_run_ctx2 = Context(
        mock_invocation_context,
        parent_ctx=root_ctx,  # Parent is root_ctx!
        node=root,
        run_id="2",
        event_actions=EventActions(),
    )
    root_run_ctx2.output = "final_root_output"

    mock_scheduler.side_effect = [
        root_run_ctx,
        child_run_ctx,
        grandchild_run_ctx,
        child_run_ctx2,
        root_run_ctx2,
    ]

    # Act
    result = await root_ctx.run_node(root, node_input="start")

    # Assert
    assert result == "final_root_output"
    assert mock_scheduler.call_count == 5

    # Verify call sequence
    calls = mock_scheduler.call_args_list

    # 1. root (run_id 1)
    assert calls[0][0][0] is root_ctx
    assert calls[0][0][1].name == root.name
    assert calls[0][1].get("run_id") == "1"

    # 2. child (run_id 1, under root_run_ctx)
    assert calls[1][0][0] is root_run_ctx
    assert calls[1][0][1].name == child.name
    assert calls[1][1].get("run_id") == "1"

    # 3. grandchild (run_id 1, under child_run_ctx)
    assert calls[2][0][0] is child_run_ctx
    assert calls[2][0][1].name == grandchild.name
    assert calls[2][1].get("run_id") == "1"

    # 4. child (run_id 2, under root_run_ctx - climbed up!)
    assert calls[3][0][0] is root_run_ctx
    assert calls[3][0][1].name == child.name
    assert calls[3][1].get("run_id") == "2"

    # 5. root (run_id 2, under root_ctx - climbed up!)
    assert calls[4][0][0] is root_ctx
    assert calls[4][0][1].name == root.name
    assert calls[4][1].get("run_id") == "2"

  @pytest.mark.asyncio
  async def test_transfer_preserves_use_as_output_for_original_context(
      self, mock_invocation_context
  ):
    """Verify that use_as_output is preserved when transferring back to self."""
    # Arrange
    from google.adk.agents.llm_agent import LlmAgent
    from google.adk.events.event_actions import EventActions

    # Create root and child agents
    child = LlmAgent(name="child", rerun_on_resume=True)
    root = LlmAgent(name="root", sub_agents=[child], rerun_on_resume=True)
    child.parent_agent = root

    # Create root context
    root_ctx = Context(mock_invocation_context, node=None, parent_ctx=None)
    mock_scheduler = AsyncMock()
    root_ctx._workflow_scheduler = mock_scheduler

    # Step 1: Root runs, transfers to child
    root_run_ctx1 = Context(
        mock_invocation_context,
        parent_ctx=root_ctx,
        node=root,
        run_id="1",
        event_actions=EventActions(transfer_to_agent="child"),
    )

    # Step 2: Child runs, transfers back to root
    child_run_ctx = Context(
        mock_invocation_context,
        parent_ctx=root_run_ctx1,
        node=child,
        run_id="1",
        event_actions=EventActions(transfer_to_agent="root"),
    )

    # Step 3: Root runs again, completes
    root_run_ctx2 = Context(
        mock_invocation_context,
        parent_ctx=root_ctx,
        node=root,
        run_id="2",
        event_actions=EventActions(),
    )
    root_run_ctx2.output = "final_output"

    mock_scheduler.side_effect = [
        root_run_ctx1,
        child_run_ctx,
        root_run_ctx2,
    ]

    # Act
    result = await root_ctx.run_node(
        root, node_input="start", use_as_output=True
    )

    # Assert
    assert result == "final_output"
    assert mock_scheduler.call_count == 3

    calls = mock_scheduler.call_args_list

    # 1. root (run_id 1) should have use_as_output=True
    assert calls[0][1].get("use_as_output") is True

    # 2. child (run_id 1, under root_run_ctx1) should have use_as_output=False
    assert calls[1][1].get("use_as_output") is False

    # 3. root (run_id 2, under root_ctx) should have use_as_output=True again!
    assert calls[2][1].get("use_as_output") is True
