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

"""Tests for _LlmAgentWrapper.

Verifies that _LlmAgentWrapper correctly adapts V1 LlmAgent for use as a
workflow graph node, covering mode validation, input conversion,
content isolation, output extraction, and both old/new workflow paths.
"""

from __future__ import annotations

from typing import Any

from google.adk.agents.context import Context
from google.adk.agents.llm.task._task_models import TaskResult
from google.adk.agents.llm_agent import LlmAgent
from google.adk.events.event import Event
from google.adk.events.event_actions import EventActions
from google.adk.features import FeatureName
from google.adk.features import override_feature_enabled
from google.adk.workflow import START
from google.adk.workflow._workflow import Workflow
from google.adk.workflow.utils._workflow_graph_utils import build_node
from google.genai import types
from pydantic import BaseModel
from pydantic import ValidationError
import pytest

from .workflow_testing_utils import create_parent_invocation_context
from .workflow_testing_utils import InputCapturingNode
from .workflow_testing_utils import TestingNode

# --- Fixtures ---


class StoryOutput(BaseModel):
  title: str
  content: str


class StoryInput(BaseModel):
  topic: str
  style: str = 'narrative'


def _make_agent(
    name: str = 'test_agent',
    mode: str = 'task',
    **kwargs,
) -> LlmAgent:
  return LlmAgent(
      name=name,
      model='gemini-2.5-flash',
      instruction='Test agent.',
      mode=mode,
      **kwargs,
  )


def _mock_agent_run(agent, finish_output=None, content_text=None):
  """Mocks agent.run_async to yield events. Returns a context manager."""

  async def fake_run_async(*args, **kwargs):
    if content_text:
      yield Event(
          invocation_id='inv',
          author=agent.name,
          content=types.Content(parts=[types.Part(text=content_text)]),
      )
    if finish_output is not None:
      # Task Delegation API: emit a finish_task FC followed by its FR.
      # The wrapper waits for the FR (so validation errors can drive a
      # retry) before extracting the FC's args as event.output.
      yield Event(
          invocation_id='inv',
          author=agent.name,
          content=types.Content(
              role='model',
              parts=[
                  types.Part(
                      function_call=types.FunctionCall(
                          name='finish_task',
                          id='ft-1',
                          args=(
                              finish_output
                              if isinstance(finish_output, dict)
                              else {'result': finish_output}
                          ),
                      )
                  )
              ],
          ),
      )
      yield Event(
          invocation_id='inv',
          author=agent.name,
          content=types.Content(
              role='user',
              parts=[
                  types.Part(
                      function_response=types.FunctionResponse(
                          name='finish_task',
                          id='ft-1',
                          response={'result': 'Task completed.'},
                      )
                  )
              ],
          ),
      )

  original = agent.run_async
  object.__setattr__(agent, 'run_async', fake_run_async)

  class _Ctx:

    def __enter__(self):
      return self

    def __exit__(self, *args):
      object.__setattr__(agent, 'run_async', original)

  return _Ctx()


def _mock_leaf_run(agent, content_text=None):
  """Mocks the agent.run_async. Returns a context manager."""
  target = agent

  async def fake_run_async(*args, **kwargs):
    if content_text:
      yield Event(output=content_text)

  original = target.run_async
  object.__setattr__(target, 'run_async', fake_run_async)

  class _Ctx:

    def __enter__(self):
      return self

    def __exit__(self, *args):
      object.__setattr__(target, 'run_async', original)

  return _Ctx()


def _new_workflow_runner(wf, test_name):
  """Creates an InMemoryRunner for the new Workflow (root_agent path)."""
  from google.adk.apps.app import App

  from . import testing_utils

  app = App(name=test_name, root_agent=wf)
  return testing_utils.InMemoryRunner(app=app)


# --- Validation ---


class TestValidation:

  def test_task_mode_accepted(self):
    """Wrapping a task-mode agent succeeds."""
    wrapper = build_node(_make_agent(mode='task'))
    assert wrapper.name == 'test_agent'

  def test_single_turn_mode_accepted(self):
    """Wrapping a single_turn-mode agent succeeds."""
    wrapper = build_node(_make_agent(mode='single_turn'))
    assert wrapper.name == 'test_agent'

  def test_chat_mode_accepted(self):
    """Wrapping a chat-mode agent succeeds."""
    wrapper = build_node(_make_agent(mode='chat'))
    assert wrapper.name == 'test_agent'

  def test_name_defaults_to_agent_name(self):
    """Wrapper name defaults to the inner agent's name."""
    wrapper = build_node(_make_agent(name='my_agent'))
    assert wrapper.name == 'my_agent'

  def test_name_can_be_overridden(self):
    """Explicit name overrides the agent's name."""
    wrapper = build_node(_make_agent(name='my_agent'), name='custom')
    assert wrapper.name == 'custom'

  def test_task_mode_waits_for_output(self):
    """Task mode sets wait_for_output=True."""
    wrapper = build_node(_make_agent(mode='task'))
    assert wrapper.wait_for_output is True

  def test_single_turn_does_not_wait_for_output(self):
    """Single_turn mode does not set wait_for_output."""
    wrapper = build_node(_make_agent(mode='single_turn'))
    assert wrapper.wait_for_output is False

  def test_rerun_on_resume_defaults_true(self):
    """Wrapper defaults to rerun_on_resume=True."""
    wrapper = build_node(_make_agent())
    assert wrapper.rerun_on_resume is True


@pytest.mark.asyncio
async def test_single_turn_input_event_inherits_branch_and_scope(
    request: pytest.FixtureRequest,
):
  """Private single-turn node input is scoped to the node branch."""
  from google.adk.workflow._llm_agent_wrapper import prepare_llm_agent_input

  agent = _make_agent(mode='single_turn')
  ic = await create_parent_invocation_context(request.function.__name__, agent)
  ic.branch = 'parent.worker@1'
  ctx = Context(invocation_context=ic)
  ctx.isolation_scope = 'scope-1'

  prepare_llm_agent_input(agent, ctx, 'hello')

  event = ic.session.events[-1]
  assert event.author == 'user'
  assert event.content and event.content.role == 'user'
  assert event.branch == 'parent.worker@1'
  assert event.isolation_scope == 'scope-1'


# --- build_node auto-wrapping ---


class TestBuildNode:

  def test_task_mode_wrapped(self):
    """build_node returns a cloned task-mode LlmAgent."""
    agent = _make_agent(mode='task')
    node = build_node(agent)
    assert isinstance(node, LlmAgent)
    assert node is not agent
    assert node.name == agent.name

  def test_single_turn_mode_wrapped(self):
    """build_node returns a cloned single_turn-mode LlmAgent."""
    node = build_node(_make_agent(mode='single_turn'))
    assert isinstance(node, LlmAgent)

  @pytest.mark.skip(
      reason=(
          'V2 LlmAgent does not allow mode=None and defaults to chat, so'
          ' fallback in wrapper is not triggered here.'
      )
  )
  def test_default_mode_auto_set_to_single_turn(self):
    """LlmAgent with explicit mode=None is auto-converted to single_turn."""
    agent = LlmAgent(
        name='agent', model='gemini-2.5-flash', instruction='Test.', mode=None
    )

    node = build_node(agent)

    assert node.mode == 'single_turn'

  @pytest.mark.parametrize(
      ('agent_kwargs', 'expected_include_contents'),
      [
          ({}, 'none'),
          ({'mode': 'single_turn'}, 'none'),
          (
              {'mode': 'single_turn', 'include_contents': 'default'},
              'default',
          ),
          ({'mode': 'single_turn', 'include_contents': 'none'}, 'none'),
      ],
  )
  @pytest.mark.asyncio
  async def test_single_turn_defaults_include_contents_only_when_unset(
      self,
      monkeypatch: pytest.MonkeyPatch,
      agent_kwargs: dict[str, Any],
      expected_include_contents: str,
  ):
    """Single-turn workflow nodes preserve explicit content inclusion."""
    from unittest.mock import MagicMock

    from google.adk.workflow import _llm_agent_wrapper

    agent = LlmAgent(
        name='test_agent',
        model='gemini-2.5-flash',
        instruction='Test.',
        **agent_kwargs,
    )
    wrapper = build_node(agent)
    seen_include_contents = []

    async def mock_run_async(*args, **kwargs):
      seen_include_contents.append(wrapper.include_contents)
      yield Event(
          invocation_id='inv',
          author=wrapper.name,
          content=types.Content(parts=[types.Part(text='ok')]),
      )

    object.__setattr__(wrapper, 'run_async', mock_run_async)
    monkeypatch.setattr(
        _llm_agent_wrapper,
        'prepare_llm_agent_context',
        lambda agent, ctx: ctx,
    )
    monkeypatch.setattr(
        _llm_agent_wrapper,
        'prepare_llm_agent_input',
        lambda agent, ctx, node_input: None,
    )
    ctx = MagicMock(spec=Context)
    ic = MagicMock()
    ctx.get_invocation_context.return_value = ic
    ic.model_copy.return_value = ic

    events = [
        event async for event in wrapper._run_impl(ctx=ctx, node_input='hi')
    ]

    assert seen_include_contents == [expected_include_contents]
    assert wrapper.include_contents == expected_include_contents
    assert events[0].content.parts[0].text == 'ok'

  def test_name_override(self):
    """build_node respects explicit name override."""
    node = build_node(_make_agent(mode='task'), name='override')
    assert node.name == 'override'


# --- Old workflow path ---


@pytest.mark.asyncio
async def test_task_finish_output_reaches_downstream(
    request: pytest.FixtureRequest,
):
  """Task mode extracts finish_task output for downstream nodes."""
  agent = _make_agent(mode='task')
  from . import testing_utils

  wrapper = build_node(agent)
  capture = InputCapturingNode(name='capture')
  wf = Workflow(
      name='wf',
      edges=[('START', wrapper), (wrapper, capture)],
  )
  runner = _new_workflow_runner(wf, request.function.__name__)

  agent_clone = next(n for n in wf.graph.nodes if n.name == wrapper.name)
  with _mock_agent_run(
      agent_clone,
      finish_output={'title': 'Story', 'content': 'Once upon a time'},
      content_text='Writing...',
  ):
    await runner.run_async(testing_utils.get_user_content('start'))

  assert capture.received_inputs == [
      {'title': 'Story', 'content': 'Once upon a time'}
  ]


@pytest.mark.asyncio
async def test_single_turn_output_reaches_downstream(
    request: pytest.FixtureRequest,
):
  """Single_turn output flows to downstream nodes."""
  from . import testing_utils

  agent = _make_agent(mode='single_turn')
  wrapper = build_node(agent)
  capture = InputCapturingNode(name='capture')
  wf = Workflow(
      name='wf',
      edges=[('START', wrapper), (wrapper, capture)],
  )
  runner = _new_workflow_runner(wf, request.function.__name__)

  agent_clone = next(n for n in wf.graph.nodes if n.name == wrapper.name)
  with _mock_leaf_run(agent_clone, content_text='Done.'):
    await runner.run_async(testing_utils.get_user_content('start'))

  assert capture.received_inputs == ['Done.']


@pytest.mark.asyncio
async def test_valid_input_schema_accepted(
    request: pytest.FixtureRequest,
):
  """Valid dict matching input_schema passes through without error."""
  from . import testing_utils

  agent = _make_agent(mode='task', input_schema=StoryInput)
  wrapper = build_node(agent)
  capture = InputCapturingNode(name='capture')
  wf = Workflow(
      name='wf',
      edges=[('START', wrapper), (wrapper, capture)],
  )
  runner = _new_workflow_runner(wf, request.function.__name__)

  agent_clone = next(n for n in wf.graph.nodes if n.name == wrapper.name)
  with _mock_agent_run(agent_clone, finish_output={'result': 'ok'}):
    await runner.run_async('{"topic": "Gemini"}')

  assert capture.received_inputs == [{'result': 'ok'}]


# Skipping this test as _LlmAgentWrapper does not seem to validate input schema
# @pytest.mark.asyncio
# async def test_invalid_input_schema_raises(
#     request: pytest.FixtureRequest,
# ):
#   """Invalid input not matching input_schema raises ValidationError."""
#   agent = _make_agent(mode='task', input_schema=StoryInput)
#   wrapper = build_node(agent)
#   wf = Workflow(name='wf', edges=[(START, wrapper)])
#   ctx = await create_parent_invocation_context(request.function.__name__, wf)
#   ic = ctx.model_copy(update={'branch': None})
#   agent_ctx = Context(invocation_context=ic, node_path='wf', run_id='exec')
#
#   with _mock_agent_run(agent, finish_output={'result': 'ok'}):
#     with pytest.raises(ValidationError):
#       async for _ in wrapper.run(ctx=agent_ctx, node_input={'style': 'comedy'}):
#         pass


@pytest.mark.asyncio
async def test_auto_wrap_in_workflow_edges(request: pytest.FixtureRequest):
  """LlmAgent placed directly in edges is auto-wrapped and works."""
  from . import testing_utils

  agent = _make_agent(mode='task')
  capture = InputCapturingNode(name='capture')
  wf = Workflow(
      name='wf',
      edges=[('START', agent), (agent, capture)],
  )
  runner = _new_workflow_runner(wf, request.function.__name__)

  agent_clone = next(n for n in wf.graph.nodes if n.name == agent.name)
  with _mock_agent_run(agent_clone, finish_output={'result': 'auto'}):
    await runner.run_async(testing_utils.get_user_content('start'))

  assert capture.received_inputs == [{'result': 'auto'}]


@pytest.mark.asyncio
async def test_single_turn_isolates_content_via_branch(
    request: pytest.FixtureRequest,
):
  """Single_turn wrapper sets a branch for content isolation."""
  agent = _make_agent(mode='single_turn')
  wrapper = build_node(agent)
  captured_branches = []

  async def fake_run(invocation_context):
    captured_branches.append(invocation_context.branch)
    yield Event(output='response')

  from . import testing_utils

  wf = Workflow(name='wf', edges=[('START', wrapper)])
  runner = _new_workflow_runner(wf, request.function.__name__)

  agent_clone = next(n for n in wf.graph.nodes if n.name == wrapper.name)
  original = agent_clone.run_async
  object.__setattr__(agent_clone, 'run_async', fake_run)
  try:
    await runner.run_async(testing_utils.get_user_content('start'))
  finally:
    object.__setattr__(agent_clone, 'run_async', original)

  assert len(captured_branches) == 1
  assert captured_branches[0] is None


@pytest.mark.asyncio
async def test_single_turn_propagates_isolation_scope(
    request: pytest.FixtureRequest,
):
  """Single-turn workflow node propagates isolation_scope to the agent."""
  agent = _make_agent(mode='single_turn')
  wrapper = build_node(agent)
  captured_isolation_scopes = []

  async def fake_run_async(invocation_context):
    captured_isolation_scopes.append(invocation_context.isolation_scope)
    yield Event(
        invocation_id='inv',
        author=wrapper.name,
        content=types.Content(parts=[types.Part(text='ok')]),
    )

  object.__setattr__(wrapper, 'run_async', fake_run_async)

  # Use the helper to create a real InvocationContext
  ic = await create_parent_invocation_context(
      request.function.__name__, wrapper
  )

  # Create the parent context with isolation_scope
  ctx = Context(invocation_context=ic)
  ctx.isolation_scope = 'test-scope-123'

  # Run the node
  events = [
      event async for event in wrapper._run_impl(ctx=ctx, node_input='hi')
  ]

  assert len(events) == 1
  assert events[0].content.parts[0].text == 'ok'
  assert captured_isolation_scopes == ['test-scope-123']


@pytest.mark.asyncio
async def test_task_mode_does_not_set_branch(
    request: pytest.FixtureRequest,
):
  """Task mode preserves None branch for HITL visibility."""
  agent = _make_agent(mode='task')
  wrapper = build_node(agent)
  captured_branches = []

  async def fake_run(invocation_context):
    captured_branches.append(invocation_context.branch)
    yield Event(
        invocation_id='inv',
        author=agent.name,
        content=types.Content(
            role='model',
            parts=[
                types.Part(
                    function_call=types.FunctionCall(
                        name='finish_task',
                        id='ft-2',
                        args={'output': {'result': 'done'}},
                    )
                )
            ],
        ),
    )

  from . import testing_utils

  wf = Workflow(name='wf', edges=[('START', wrapper)])
  runner = _new_workflow_runner(wf, request.function.__name__)

  agent_clone = next(n for n in wf.graph.nodes if n.name == wrapper.name)
  original = agent_clone.run_async
  object.__setattr__(agent_clone, 'run_async', fake_run)
  try:
    await runner.run_async(testing_utils.get_user_content('start'))
  finally:
    object.__setattr__(agent_clone, 'run_async', original)

  assert captured_branches == [None]


@pytest.mark.asyncio
async def test_single_turn_converts_input_to_content(
    request: pytest.FixtureRequest,
):
  """Single_turn wrapper converts string node_input to types.Content."""
  agent = _make_agent(mode='single_turn')
  wrapper = build_node(agent)
  captured_inputs = []

  async def fake_run(*args, **kwargs):
    ctx = args[0]
    captured_inputs.append(ctx.session.events[-1].message)
    yield Event(output='response')

  from . import testing_utils

  predecessor = TestingNode(name='pred', output='hello world')
  wf = Workflow(
      name='wf',
      edges=[('START', predecessor), (predecessor, wrapper)],
  )
  runner = _new_workflow_runner(wf, request.function.__name__)

  agent_clone = next(n for n in wf.graph.nodes if n.name == wrapper.name)
  original = agent_clone.run_async
  object.__setattr__(agent_clone, 'run_async', fake_run)
  try:
    await runner.run_async(testing_utils.get_user_content('start'))
  finally:
    object.__setattr__(agent_clone, 'run_async', original)

  assert len(captured_inputs) == 1
  assert isinstance(captured_inputs[0], types.Content)
  assert captured_inputs[0].parts[0].text == 'hello world'


# --- New workflow path ---


def _get_user_content():
  from . import testing_utils

  return testing_utils.get_user_content


@pytest.mark.asyncio
async def test_react_path_user_content_visible_to_llm(
    request: pytest.FixtureRequest,
):
  """First-node LLM agent sees the user message in the new Workflow."""
  from google.adk.workflow._workflow import Workflow as NewWorkflow

  from . import testing_utils

  mock_model = testing_utils.MockModel.create(responses=['extracted output'])
  agent = LlmAgent(
      name='process_request',
      model=mock_model,
      instruction='Extract info from the user message.',
  )
  wf = NewWorkflow(name='wf', edges=[('START', agent)])

  runner = _new_workflow_runner(wf, request.function.__name__)
  await runner.run_async(
      testing_utils.get_user_content('I want 3 days off for vacation')
  )

  assert len(mock_model.requests) == 1
  user_texts = [
      p.text
      for c in mock_model.requests[0].contents
      if c.role == 'user'
      for p in c.parts or []
      if p.text
  ]
  assert any('3 days' in t for t in user_texts)


@pytest.mark.skip(
    reason=(
        '_LlmAgentWrapper does not fully support new workflow path in this test'
    )
)
@pytest.mark.asyncio
async def test_react_path_output_reaches_downstream(
    request: pytest.FixtureRequest,
):
  """LLM output flows to the next node in the new Workflow."""
  from google.adk.workflow._workflow import Workflow as NewWorkflow

  from . import testing_utils

  mock_model = testing_utils.MockModel.create(responses=['hello world'])
  agent = LlmAgent(
      name='greeter',
      model=mock_model,
      instruction='Greet.',
  )
  captured = []

  def capture(node_input: str):
    captured.append(node_input)

  wf = NewWorkflow(name='wf', edges=[('START', agent, capture)])

  runner = _new_workflow_runner(wf, request.function.__name__)
  await runner.run_async(testing_utils.get_user_content('hi'))

  assert captured == ['hello world']


@pytest.mark.skip(
    reason=(
        '_LlmAgentWrapper does not fully support new workflow path in this test'
    )
)
@pytest.mark.asyncio
async def test_react_path_output_key_stored_in_state(
    request: pytest.FixtureRequest,
):
  """output_key stores LLM output in state in the new Workflow."""
  from google.adk.workflow._workflow import Workflow as NewWorkflow

  from . import testing_utils

  mock_model = testing_utils.MockModel.create(responses=['summary text'])
  agent = LlmAgent(
      name='summarizer',
      model=mock_model,
      instruction='Summarize.',
      output_key='summary',
  )
  captured_state = []

  def check_state(ctx: Context):
    captured_state.append(ctx.state.get('summary'))

  wf = NewWorkflow(name='wf', edges=[('START', agent, check_state)])

  runner = _new_workflow_runner(wf, request.function.__name__)
  await runner.run_async(testing_utils.get_user_content('some text'))

  assert captured_state == ['summary text']


@pytest.mark.skip(
    reason=(
        '_LlmAgentWrapper does not fully support new workflow path in this test'
    )
)
@pytest.mark.asyncio
async def test_react_path_output_schema_validated(
    request: pytest.FixtureRequest,
):
  """output_schema is validated and parsed in the new Workflow."""
  from google.adk.workflow._workflow import Workflow as NewWorkflow

  from . import testing_utils

  mock_model = testing_utils.MockModel.create(
      responses=['{"title": "My Story", "content": "Once upon a time"}']
  )
  agent = LlmAgent(
      name='writer',
      model=mock_model,
      instruction='Write a story.',
      output_schema=StoryOutput,
      output_key='story',
  )
  captured = []

  def check_output(node_input: dict):
    captured.append(node_input)

  wf = NewWorkflow(name='wf', edges=[('START', agent, check_output)])

  runner = _new_workflow_runner(wf, request.function.__name__)
  await runner.run_async(testing_utils.get_user_content('write'))

  assert len(captured) == 1
  assert captured[0]['title'] == 'My Story'
  assert captured[0]['content'] == 'Once upon a time'


@pytest.mark.skip(
    reason=(
        '_LlmAgentWrapper does not fully support new workflow path in this test'
    )
)
@pytest.mark.asyncio
async def test_react_path_predecessor_input_visible_to_llm(
    request: pytest.FixtureRequest,
):
  """Predecessor output is injected as user content for the LLM."""
  from google.adk.workflow._workflow import Workflow as NewWorkflow

  from . import testing_utils

  mock_model = testing_utils.MockModel.create(responses=['processed'])
  agent = LlmAgent(
      name='processor',
      model=mock_model,
      instruction='Process.',
  )

  def step_one(node_input: str) -> str:
    return 'transformed data'

  wf = NewWorkflow(name='wf', edges=[('START', step_one, agent)])

  runner = _new_workflow_runner(wf, request.function.__name__)
  await runner.run_async(testing_utils.get_user_content('raw input'))

  assert len(mock_model.requests) == 1
  user_texts = [
      p.text
      for c in mock_model.requests[0].contents
      if c.role == 'user'
      for p in c.parts or []
      if p.text
  ]
  assert any('transformed data' in t for t in user_texts)


# --- React path: interrupt and resume ---


@pytest.mark.skip(
    reason=(
        '_LlmAgentWrapper does not fully support new workflow path in this test'
    )
)
@pytest.mark.asyncio
async def test_long_running_tool_interrupts_workflow(
    request: pytest.FixtureRequest,
):
  """Long-running tool stops the workflow after one LLM call."""
  from google.adk.tools.long_running_tool import LongRunningFunctionTool
  from google.adk.workflow._workflow import Workflow as NewWorkflow

  from . import testing_utils

  def approve(request: str) -> None:
    """Approve a request (long-running)."""
    return None

  fc = types.Part.from_function_call(name='approve', args={'request': 'deploy'})
  mock_model = testing_utils.MockModel.create(responses=[fc])
  agent = LlmAgent(
      name='approver',
      model=mock_model,
      instruction='Get approval.',
      tools=[LongRunningFunctionTool(approve)],
  )
  wf = NewWorkflow(name='wf', edges=[('START', agent)])

  runner = _new_workflow_runner(wf, request.function.__name__)
  events = await runner.run_async(testing_utils.get_user_content('deploy'))

  assert len(mock_model.requests) == 1
  assert any(e.long_running_tool_ids for e in events)


@pytest.mark.skip(
    reason=(
        '_LlmAgentWrapper does not fully support new workflow path in this test'
    )
)
@pytest.mark.asyncio
async def test_resume_after_interrupt_completes_workflow(
    request: pytest.FixtureRequest,
):
  """Resuming after interrupt calls the LLM once more to complete."""
  from google.adk.apps.app import App
  from google.adk.apps.app import ResumabilityConfig
  from google.adk.tools.long_running_tool import LongRunningFunctionTool
  from google.adk.workflow._workflow import Workflow as NewWorkflow

  from . import testing_utils

  def approve(request: str) -> None:
    """Approve a request (long-running)."""
    return None

  fc = types.Part.from_function_call(name='approve', args={'request': 'deploy'})
  mock_model = testing_utils.MockModel.create(
      responses=[fc, 'Approved and deployed.']
  )
  agent = LlmAgent(
      name='approver',
      model=mock_model,
      instruction='Get approval.',
      tools=[LongRunningFunctionTool(approve)],
  )
  wf = NewWorkflow(name='wf', edges=[('START', agent)])

  app = App(
      name=request.function.__name__,
      root_agent=wf,
      resumability_config=ResumabilityConfig(is_resumable=True),
  )
  runner = testing_utils.InMemoryRunner(app=app)

  # Run 1: LLM → FC → interrupt
  events1 = await runner.run_async(
      testing_utils.get_user_content('deploy please')
  )
  invocation_id = events1[0].invocation_id
  assert any(e.long_running_tool_ids for e in events1)

  # Find the interrupt FC id
  interrupt_event = next(e for e in events1 if e.long_running_tool_ids)
  fc_id = list(interrupt_event.long_running_tool_ids)[0]

  # Run 2: Resume with FR
  resume_msg = types.Content(
      role='user',
      parts=[
          types.Part(
              function_response=types.FunctionResponse(
                  name='approve',
                  id=fc_id,
                  response={'result': 'yes'},
              )
          )
      ],
  )
  events2 = await runner.run_async(
      new_message=resume_msg,
      invocation_id=invocation_id,
  )

  # Total LLM calls: 1 (first run) + 1 (resume) = 2.
  assert len(mock_model.requests) == 2
  # Verify resumed output reached completion.
  content_texts = [
      p.text
      for e in events2
      if e.content and e.content.parts
      for p in e.content.parts
      if p.text
  ]
  assert any('Approved and deployed.' in t for t in content_texts)


@pytest.mark.skip(
    reason=(
        '_LlmAgentWrapper does not fully support new workflow path in this test'
    )
)
@pytest.mark.asyncio
async def test_multiple_sequential_interrupts_in_workflow(
    request: pytest.FixtureRequest,
):
  """Two interrupts in sequence each resume and complete in a workflow."""
  from google.adk.apps.app import App
  from google.adk.apps.app import ResumabilityConfig
  from google.adk.tools.long_running_tool import LongRunningFunctionTool
  from google.adk.workflow._workflow import Workflow as NewWorkflow

  from . import testing_utils

  def step_one() -> None:
    """First long-running step."""
    return None

  def step_two() -> None:
    """Second long-running step."""
    return None

  fc1 = types.Part.from_function_call(name='step_one', args={})
  fc2 = types.Part.from_function_call(name='step_two', args={})
  mock_model = testing_utils.MockModel.create(responses=[fc1, fc2, 'All done.'])
  agent = LlmAgent(
      name='worker',
      model=mock_model,
      instruction='Do two steps.',
      tools=[
          LongRunningFunctionTool(step_one),
          LongRunningFunctionTool(step_two),
      ],
  )
  wf = NewWorkflow(name='wf', edges=[('START', agent)])

  app = App(
      name=request.function.__name__,
      root_agent=wf,
      resumability_config=ResumabilityConfig(is_resumable=True),
  )
  runner = testing_utils.InMemoryRunner(app=app)

  # Run 1: LLM → FC1 → interrupt
  events1 = await runner.run_async(testing_utils.get_user_content('Start'))
  assert any(e.long_running_tool_ids for e in events1)
  invocation_id = events1[0].invocation_id
  interrupt1 = next(e for e in events1 if e.long_running_tool_ids)
  fc1_id = list(interrupt1.long_running_tool_ids)[0]

  # Run 2: Resume FC1 → LLM → FC2 → interrupt again
  events2 = await runner.run_async(
      new_message=types.Content(
          role='user',
          parts=[
              types.Part(
                  function_response=types.FunctionResponse(
                      name='step_one',
                      id=fc1_id,
                      response={'result': 'step1 done'},
                  )
              )
          ],
      ),
      invocation_id=invocation_id,
  )
  assert any(e.long_running_tool_ids for e in events2)
  assert len(mock_model.requests) == 2
  interrupt2 = next(e for e in events2 if e.long_running_tool_ids)
  fc2_id = list(interrupt2.long_running_tool_ids)[0]

  # Run 3: Resume FC2 → LLM → text → done
  invocation_id2 = events2[0].invocation_id
  events3 = await runner.run_async(
      new_message=types.Content(
          role='user',
          parts=[
              types.Part(
                  function_response=types.FunctionResponse(
                      name='step_two',
                      id=fc2_id,
                      response={'result': 'step2 done'},
                  )
              )
          ],
      ),
      invocation_id=invocation_id2,
  )

  # Total: 3 LLM calls (one per run).
  assert len(mock_model.requests) == 3
  content_texts = [
      p.text
      for e in events3
      if e.content and e.content.parts
      for p in e.content.parts
      if p.text
  ]
  assert any('All done.' in t for t in content_texts)


# --- Original tests from test_v1_llm_agent_wrapper.py ---


def _make_v1_agent(mode='task'):
  return LlmAgent(
      name='test_v1_agent',
      model='gemini-2.5-flash',
      instruction='Test instruction',
      mode=mode,
  )


def test_task_mode_sets_wait_for_output():
  agent = _make_v1_agent(mode='task')
  wrapper = build_node(agent)
  assert wrapper.wait_for_output is True


def test_single_turn_does_not_set_wait_for_output():
  agent = _make_v1_agent(mode='single_turn')
  wrapper = build_node(agent)
  assert wrapper.wait_for_output is False


def test_chat_mode_sets_wait_for_output():
  agent = _make_v1_agent(mode='chat')
  wrapper = build_node(agent)
  assert wrapper.wait_for_output is True


@pytest.mark.asyncio
async def test_task_mode_proceeds_on_finish_task():
  agent = _make_v1_agent(mode='task')
  wrapper = build_node(agent)

  async def mock_run_async(*args, **kwargs):
    yield Event(
        invocation_id='inv',
        author='test_v1_agent',
        content=types.Content(
            role='model',
            parts=[
                types.Part(
                    function_call=types.FunctionCall(
                        name='finish_task',
                        id='ft-3',
                        args={'output': 'done_output'},
                    )
                )
            ],
        ),
    )
    yield Event(
        invocation_id='inv',
        author='test_v1_agent',
        content=types.Content(
            role='user',
            parts=[
                types.Part(
                    function_response=types.FunctionResponse(
                        name='finish_task',
                        id='ft-3',
                        response={'result': 'Task completed.'},
                    )
                )
            ],
        ),
    )

  object.__setattr__(wrapper, 'run_async', mock_run_async)

  from unittest.mock import AsyncMock
  from unittest.mock import MagicMock

  ctx = MagicMock(spec=Context)
  ic = MagicMock()
  ctx.get_invocation_context.return_value = ic
  ic.model_copy.return_value = ic
  ic.plugin_manager.run_before_agent_callback = AsyncMock(return_value=None)
  ic.plugin_manager.run_after_agent_callback = AsyncMock(return_value=None)
  ctx.node_path = 'wf'

  events = []
  async for e in wrapper._run_impl(ctx=ctx, node_input='hello'):
    events.append(e)

  # Wrapper yields both the FC and the success FR; output is set on the FR.
  assert len(events) == 2
  assert events[1].output == {'output': 'done_output'}


@pytest.mark.asyncio
async def test_task_mode_does_not_proceed_without_finish_task():
  agent = _make_v1_agent(mode='task')
  wrapper = build_node(agent)

  async def mock_run_async(*args, **kwargs):
    yield Event(
        invocation_id='inv',
        author='test_v1_agent',
        content=types.Content(parts=[types.Part(text='Working...')]),
    )

  object.__setattr__(wrapper, 'run_async', mock_run_async)

  from unittest.mock import AsyncMock
  from unittest.mock import MagicMock

  ctx = MagicMock(spec=Context)
  ic = MagicMock()
  ctx.get_invocation_context.return_value = ic
  ic.model_copy.return_value = ic
  ic.plugin_manager.run_before_agent_callback = AsyncMock(return_value=None)
  ic.plugin_manager.run_after_agent_callback = AsyncMock(return_value=None)
  ctx.node_path = 'wf'

  events = []
  async for e in wrapper._run_impl(ctx=ctx, node_input='hello'):
    events.append(e)

  assert len(events) == 1
  assert events[0].output is None


@pytest.mark.asyncio
async def test_chat_mode_yields_events_directly():
  agent = _make_v1_agent(mode='chat')
  wrapper = build_node(agent)

  async def mock_run_async(*args, **kwargs):
    yield Event(
        invocation_id='inv',
        author='test_v1_agent',
        content=types.Content(parts=[types.Part(text='Hello from chat')]),
    )

  object.__setattr__(wrapper, 'run_async', mock_run_async)

  from unittest.mock import AsyncMock
  from unittest.mock import MagicMock

  ctx = MagicMock(spec=Context)
  ic = MagicMock()
  ctx.get_invocation_context.return_value = ic
  ic.model_copy.return_value = ic
  ic.plugin_manager.run_before_agent_callback = AsyncMock(return_value=None)
  ic.plugin_manager.run_after_agent_callback = AsyncMock(return_value=None)
  ctx.node_path = 'wf'

  events = []
  async for e in wrapper._run_impl(ctx=ctx, node_input='hello'):
    events.append(e)

  assert len(events) == 1
  assert events[0].content.parts[0].text == 'Hello from chat'
  assert events[0].output is None


def test_chat_mode_agent_following_non_start_raises_validation_error():
  """Wiring a chat-mode agent following a non-START node raises ValueError."""
  agent = _make_v1_agent(mode='chat')
  predecessor = TestingNode(name='pred', output='some output')

  with pytest.raises(ValueError) as exc_info:
    Workflow(
        name='wf',
        edges=[('START', predecessor), (predecessor, agent)],
    )

  assert (
      "The agent 'test_v1_agent' has been added to the workflow with"
      " mode='chat' following node 'pred'."
      in str(exc_info.value)
  )


def test_chat_mode_agent_from_start_allowed():
  """Wiring a chat-mode agent directly from START is allowed and validated without error."""
  agent = _make_v1_agent(mode='chat')

  wf = Workflow(
      name='wf',
      edges=[('START', agent)],
  )
  assert wf.graph is not None


@pytest.mark.asyncio
async def test_three_layer_llm_agent_transfer_round_trip(
    request: pytest.FixtureRequest,
):
  """Verify 3-layer LlmAgent transfers end-to-end (Root -> Child -> Grandchild -> Child -> Root)."""
  from google.adk.apps.app import App
  from google.adk.apps.app import ResumabilityConfig

  from . import testing_utils

  # Prepare the transfer function call parts
  fc_transfer_to_child = types.Part.from_function_call(
      name='transfer_to_agent',
      args={'agent_name': 'child_agent'},
  )
  fc_transfer_to_grandchild = types.Part.from_function_call(
      name='transfer_to_agent',
      args={'agent_name': 'grandchild_agent'},
  )
  fc_transfer_to_child_parent = types.Part.from_function_call(
      name='transfer_to_agent',
      args={'agent_name': 'child_agent'},
  )
  fc_transfer_to_root = types.Part.from_function_call(
      name='transfer_to_agent',
      args={'agent_name': 'root_agent'},
  )

  # Mock models for 3 layers
  root_model = testing_utils.MockModel.create(
      responses=[fc_transfer_to_child, 'Welcome back to root!']
  )
  child_model = testing_utils.MockModel.create(
      responses=[
          fc_transfer_to_grandchild,
          'Welcome back to child!',
          fc_transfer_to_root,
      ]
  )
  grandchild_model = testing_utils.MockModel.create(
      responses=['Hello, I am grandchild!', fc_transfer_to_child_parent]
  )

  # Instantiate agents
  grandchild_agent = LlmAgent(
      name='grandchild_agent',
      model=grandchild_model,
      instruction='Grandchild agent.',
  )
  child_agent = LlmAgent(
      name='child_agent',
      model=child_model,
      instruction='Child agent.',
      sub_agents=[grandchild_agent],
  )
  root_agent = LlmAgent(
      name='root_agent',
      model=root_model,
      instruction='Root agent.',
      sub_agents=[child_agent],
  )

  app = App(
      name=request.function.__name__,
      root_agent=root_agent,
      resumability_config=ResumabilityConfig(is_resumable=True),
  )
  runner = testing_utils.InMemoryRunner(app=app)

  # Turn 1: Start (Root -> Child -> Grandchild -> Grandchild speaks)
  events1 = await runner.run_async(testing_utils.get_user_content('Start'))
  invocation_id = events1[0].invocation_id

  # Verify Turn 1 completed at Grandchild
  content_texts1 = [
      p.text
      for e in events1
      if e.content and e.content.parts
      for p in e.content.parts
      if p.text
  ]
  assert any('Hello, I am grandchild!' in t for t in content_texts1)

  # Turn 2: Go back to parent (Grandchild -> Child -> Child speaks)
  events2 = await runner.run_async(
      new_message=testing_utils.get_user_content('Go back to parent'),
      invocation_id=invocation_id,
  )

  # Verify Turn 2 completed at Child
  content_texts2 = [
      p.text
      for e in events2
      if e.content and e.content.parts
      for p in e.content.parts
      if p.text
  ]
  assert any('Welcome back to child!' in t for t in content_texts2)

  # Turn 3: Go back to root (Child -> Root -> Root speaks)
  events3 = await runner.run_async(
      new_message=testing_utils.get_user_content('Go back to root'),
      invocation_id=invocation_id,
  )

  # Verify Turn 3 completed at Root
  content_texts3 = [
      p.text
      for e in events3
      if e.content and e.content.parts
      for p in e.content.parts
      if p.text
  ]
  assert any('Welcome back to root!' in t for t in content_texts3)


@pytest.mark.asyncio
async def test_workflow_node_with_valid_input_schema_completes_successfully(
    request: pytest.FixtureRequest,
):
  """A valid node_input payload successfully passes schema validation."""

  # Arrange
  class InputSchema(BaseModel):
    required_field: str

  agent = LlmAgent(
      name='schema_agent',
      model='test_model',
      input_schema=InputSchema,
      instruction='Just say hi',
      mode='single_turn',
  )
  wrapper = build_node(agent)
  wf = Workflow(
      name='test_workflow',
      edges=[('START', wrapper)],
  )
  runner = _new_workflow_runner(wf, request.function.__name__)
  agent_clone = next(n for n in wf.graph.nodes if n.name == wrapper.name)

  # Act
  with _mock_agent_run(agent_clone, content_text='hi'):
    events = await runner.run_async('{"required_field": "hello"}')

  # Assert
  assert len(events) > 0


@pytest.mark.asyncio
async def test_workflow_node_with_invalid_input_schema_raises_validation_error(
    request: pytest.FixtureRequest,
):
  """An invalid node_input payload raises a pydantic ValidationError."""

  # Arrange
  class InputSchema(BaseModel):
    required_field: str

  agent = LlmAgent(
      name='schema_agent',
      model='test_model',
      input_schema=InputSchema,
      instruction='Just say hi',
      mode='single_turn',
  )
  wrapper = build_node(agent)
  wf = Workflow(
      name='test_workflow',
      edges=[('START', wrapper)],
  )
  runner = _new_workflow_runner(wf, request.function.__name__)
  agent_clone = next(n for n in wf.graph.nodes if n.name == wrapper.name)

  # Act / Assert
  with _mock_agent_run(agent_clone, content_text='hi'):
    with pytest.raises(ValidationError):
      await runner.run_async('{"wrong_field": "hello"}')
