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

import copy
from typing import Any
from typing import AsyncGenerator
import uuid

from google.adk.agents.context import Context
from google.adk.agents.llm_agent import LlmAgent
from google.adk.apps.app import App
from google.adk.events.event import Event
from google.adk.events.request_input import RequestInput
from google.adk.runners import Runner
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.tools.long_running_tool import LongRunningFunctionTool
from google.adk.workflow import BaseNode
from google.adk.workflow import JoinNode
from google.adk.workflow._base_node import START
from google.adk.workflow._node_status import NodeStatus
from google.adk.workflow._workflow import Workflow
from google.adk.workflow.utils._workflow_hitl_utils import create_request_input_response
from google.adk.workflow.utils._workflow_hitl_utils import get_request_input_interrupt_ids
from google.adk.workflow.utils._workflow_hitl_utils import has_request_input_function_call
from google.genai import types
from pydantic import ConfigDict
from pydantic import Field
import pytest
from typing_extensions import override

from .. import testing_utils
from .workflow_testing_utils import find_function_call_event
from .workflow_testing_utils import InputCapturingNode
from .workflow_testing_utils import RequestInputNode
from .workflow_testing_utils import simplify_events_with_node
from .workflow_testing_utils import TestingNode


def long_running_tool_func():
  """A test tool that simulates a long-running operation."""
  return None


@pytest.mark.asyncio
async def test_nested_workflow_as_node(request: pytest.FixtureRequest):
  """Tests that a Workflow can be used as a node in another Workflow."""

  async def nested_func(node_input: types.Content):
    return 'I am nested'

  nested_agent = Workflow(
      name='nested_agent',
      edges=[('START', nested_func)],
  )

  async def output_func(node_input: str):
    return 'I am outer'

  outer_agent = Workflow(
      name='outer_agent',
      edges=[('START', nested_agent), (nested_agent, output_func)],
  )

  app = App(
      name=request.function.__name__,
      root_agent=outer_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)
  events = await runner.run_async(testing_utils.get_user_content('hello'))

  simplified_events = simplify_events_with_node(events)
  assert simplified_events == [
      (
          'outer_agent@1/nested_agent@1/nested_func@1',
          {
              'output': 'I am nested',
          },
      ),
      (
          'outer_agent@1/output_func@1',
          {
              'output': 'I am outer',
          },
      ),
  ]


@pytest.mark.asyncio
async def test_nested_workflow_with_join_node(
    request: pytest.FixtureRequest,
):
  """Tests that a nested Workflow with JoinNode works correctly."""

  async def nested_node_a():
    return {'a': 1}

  async def nested_node_b():
    return {'b': 2}

  async def nested_node_c():
    return {'c': 3}

  nested_join_node = JoinNode(name='nested_join')

  nested_agent = Workflow(
      name='nested_agent',
      edges=[
          ('START', nested_node_a),
          ('START', nested_node_c),
          (nested_node_a, nested_join_node),
          (nested_node_c, nested_node_b),
          (nested_node_b, nested_join_node),
      ],
  )

  async def output_func(node_input: dict):
    return (
        'Joined output:'
        f' a={node_input["nested_node_a"]["a"]},'
        f' b={node_input["nested_node_b"]["b"]}'
    )

  outer_agent = Workflow(
      name='outer_agent',
      edges=[('START', nested_agent), (nested_agent, output_func)],
  )

  app = App(
      name=request.function.__name__,
      root_agent=outer_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)
  events = await runner.run_async(testing_utils.get_user_content('hello'))

  simplified_events = simplify_events_with_node(events)
  assert sorted(simplified_events[0:2], key=lambda x: x[0]) == [
      (
          'outer_agent@1/nested_agent@1/nested_node_a@1',
          {'output': {'a': 1}},
      ),
      (
          'outer_agent@1/nested_agent@1/nested_node_c@1',
          {'output': {'c': 3}},
      ),
  ]
  assert simplified_events[2:] == [
      (
          'outer_agent@1/nested_agent@1/nested_node_b@1',
          {'output': {'b': 2}},
      ),
      (
          'outer_agent@1/nested_agent@1/nested_join@1',
          {
              'output': {'nested_node_a': {'a': 1}, 'nested_node_b': {'b': 2}},
          },
      ),
      (
          'outer_agent@1/output_func@1',
          {
              'output': 'Joined output: a=1, b=2',
          },
      ),
  ]


@pytest.mark.asyncio
async def test_nested_workflow_updates_state_outer_reads(
    request: pytest.FixtureRequest,
):
  """Tests that outer workflow can read state updated by nested workflow."""

  async def nested_state_updater(ctx: Context):
    yield Event(
        state={'my_key': 'my_value'},
    )
    yield 'nested agent finished'

  nested_agent = Workflow(
      name='nested_agent',
      edges=[('START', nested_state_updater)],
  )

  def outer_state_reader(my_key: str, node_input: str):
    return f'Nested agent output: {node_input}, state value: {my_key}'

  outer_agent = Workflow(
      name='outer_agent',
      edges=[('START', nested_agent), (nested_agent, outer_state_reader)],
  )

  app = App(
      name=request.function.__name__,
      root_agent=outer_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)
  events = await runner.run_async(testing_utils.get_user_content('hello'))

  simplified_events = simplify_events_with_node(events)
  assert simplified_events == [
      (
          'outer_agent@1/nested_agent@1/nested_state_updater@1',
          {
              'output': 'nested agent finished',
          },
      ),
      (
          'outer_agent@1/outer_state_reader@1',
          {
              'output': (
                  'Nested agent output: nested agent finished, state value:'
                  ' my_value'
              ),
          },
      ),
  ]


@pytest.mark.asyncio
async def test_nested_workflow_intermediate_nodes(
    request: pytest.FixtureRequest,
):
  """Tests that only the final output of a nested workflow is passed to the outer workflow."""

  node_a = TestingNode(name='NodeA', output='Inner Intermediate')
  node_b = TestingNode(name='NodeB', output='Inner Final')

  nested_agent = Workflow(
      name='nested_agent',
      edges=[
          ('START', node_a),
          (node_a, node_b),
      ],
  )

  output_node = InputCapturingNode(name='OutputNode')

  outer_agent = Workflow(
      name='outer_agent',
      edges=[
          ('START', nested_agent),
          (nested_agent, output_node),
      ],
  )

  app = App(
      name=request.function.__name__,
      root_agent=outer_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)
  events = await runner.run_async(testing_utils.get_user_content('start'))

  assert output_node.received_inputs == ['Inner Final']

  simplified_events = simplify_events_with_node(events)
  assert simplified_events == [
      (
          'outer_agent@1/nested_agent@1/NodeA@1',
          {'output': 'Inner Intermediate'},
      ),
      (
          'outer_agent@1/nested_agent@1/NodeB@1',
          {'output': 'Inner Final'},
      ),
      (
          'outer_agent@1/OutputNode@1',
          {'output': {'received': 'Inner Final'}},
      ),
  ]


@pytest.mark.asyncio
async def test_nested_workflow_with_hitl(request: pytest.FixtureRequest):
  """Tests that a nested Workflow with HITL works correctly."""
  # Given: A nested workflow with an LLM agent that calls a long running tool
  llm_agent = LlmAgent(
      name='llm_agent',
      model=testing_utils.MockModel.create(
          responses=[
              types.Part.from_function_call(
                  name='long_running_tool_func',
                  args={},
              ),
              types.Part.from_text(text='LLM response after tool'),
          ]
      ),
      tools=[LongRunningFunctionTool(func=long_running_tool_func)],
  )

  nested_agent = Workflow(
      name='nested_agent',
      edges=[('START', llm_agent)],
  )

  async def output_func(node_input: Any):
    return 'I am outer'

  outer_agent = Workflow(
      name='outer_agent',
      edges=[('START', nested_agent), (nested_agent, output_func)],
  )

  app = App(
      name=request.function.__name__,
      root_agent=outer_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)

  # When: Starting the workflow
  user_event = testing_utils.get_user_content('start workflow')
  events1 = await runner.run_async(user_event)

  # Then: It should yield a FunctionCall event and wait for tool execution
  ev = find_function_call_event(events1, 'long_running_tool_func')
  assert ev is not None
  function_call_id = ev.content.parts[0].function_call.id

  simplified_events1 = simplify_events_with_node(events1)
  assert simplified_events1 == [
      (
          'outer_agent@1/nested_agent@1/llm_agent@1',
          types.Part.from_function_call(name='long_running_tool_func', args={}),
      ),
  ]

  tool_response = testing_utils.UserContent(
      types.Part(
          function_response=types.FunctionResponse(
              id=function_call_id,
              name='long_running_tool_func',
              response={'result': 'Final tool output'},
          )
      )
  )

  invocation_id = events1[0].invocation_id
  events2 = await runner.run_async(
      new_message=tool_response,
      invocation_id=invocation_id,
  )

  simplified_events2 = simplify_events_with_node(events2)
  assert simplified_events2 == [
      ('outer_agent@1/nested_agent@1/llm_agent@1', 'LLM response after tool'),
      (
          'outer_agent@1/output_func@1',
          {'output': 'I am outer'},
      ),
  ]


@pytest.mark.asyncio
async def test_nested_workflow_with_request_input_event_hitl(
    request: pytest.FixtureRequest,
):
  """Nested workflow correctly propagates RequestInput and resumes.

  Setup: outer_agent -> nested_agent -> node_hitl.
  Act:
    - Run 1: start workflow, node_hitl yields RequestInput.
    - Run 2: resume with user response.
  Assert:
    - Run 1: returns RequestInput event.
    - Run 2: node_hitl receives input and completes.
  """

  class NodeHitl(BaseNode):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    rerun_on_resume: bool = Field(default=True)
    name: str = Field(default='node_hitl')

    @override
    async def _run_impl(
        self,
        *,
        ctx: Context,
        node_input: Any,
    ) -> AsyncGenerator[Any, None]:
      if resume_input := ctx.resume_inputs.get('request_input'):
        yield f'Resumed with user input: {resume_input}'
      else:
        yield RequestInput(
            message='requesting input via RequestInputEvent',
            interrupt_id='request_input',
        )

  # Given: A nested workflow where the inner node requests input
  node_hitl_instance = NodeHitl()

  nested_agent = Workflow(
      name='nested_agent',
      edges=[('START', node_hitl_instance)],
  )

  async def output_func():
    return 'I am outer'

  outer_agent = Workflow(
      name='outer_agent',
      edges=[('START', nested_agent), (nested_agent, output_func)],
  )

  app = App(
      name=request.function.__name__,
      root_agent=outer_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)

  # When: Starting the workflow
  user_event = testing_utils.get_user_content('start workflow')
  events1 = await runner.run_async(user_event)

  # Then: It should yield a RequestInput event
  req_events = [e for e in events1 if has_request_input_function_call(e)]
  assert req_events
  interrupt_id = get_request_input_interrupt_ids(req_events[0])[0]
  assert interrupt_id == 'request_input'

  simplified_events1 = simplify_events_with_node(events1)
  assert simplified_events1 == [
      (
          'outer_agent@1/nested_agent@1/node_hitl@1',
          testing_utils.simplify_content(copy.deepcopy(req_events[0].content)),
      ),
  ]

  # When: Resuming with user response
  hitl_response = types.Content(
      parts=[
          create_request_input_response(
              interrupt_id, {'response': 'user input for hitl'}
          )
      ],
      role='user',
  )

  invocation_id = events1[0].invocation_id
  events2 = await runner.run_async(
      new_message=hitl_response,
      invocation_id=invocation_id,
  )

  # Then: It should complete and pass output to the outer workflow
  simplified_events2 = simplify_events_with_node(events2)
  assert simplified_events2 == [
      (
          'outer_agent@1/nested_agent@1/node_hitl@1',
          {
              'output': (
                  "Resumed with user input: {'response': 'user input for hitl'}"
              ),
          },
      ),
      (
          'outer_agent@1/output_func@1',
          {'output': 'I am outer'},
      ),
  ]


@pytest.mark.asyncio
async def test_nested_agent_with_request_input_piped_to_next_node(
    request: pytest.FixtureRequest,
):
  """Tests that user response to RequestInput in nested agent is piped to next node."""
  ask_user = RequestInputNode(
      name='ask_user',
      message='Please provide input',
  )
  capture_node = InputCapturingNode(name='capture_node')

  sub_agent = Workflow(
      name='sub_agent',
      edges=[
          ('START', ask_user),
          (ask_user, capture_node),
      ],
  )

  root_agent = Workflow(
      name='root_agent',
      edges=[('START', sub_agent)],
  )

  app = App(
      name=request.function.__name__,
      root_agent=root_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)

  user_event = testing_utils.get_user_content('start workflow')
  events1 = await runner.run_async(user_event)

  req_events = [e for e in events1 if has_request_input_function_call(e)]
  assert req_events
  interrupt_id = get_request_input_interrupt_ids(req_events[0])[0]
  assert interrupt_id
  hitl_response_payload = {'response': 'user input for hitl'}
  hitl_response = types.Content(
      parts=[
          create_request_input_response(interrupt_id, hitl_response_payload)
      ],
      role='user',
  )

  invocation_id = events1[0].invocation_id
  await runner.run_async(
      new_message=hitl_response,
      invocation_id=invocation_id,
  )

  assert capture_node.received_inputs == [hitl_response_payload]


@pytest.mark.asyncio
async def test_nested_workflow_chain_input_propagation(
    request: pytest.FixtureRequest,
):

  async def create_output_a():
    return 'output of A'

  nested_agent_a = Workflow(
      name='nested_agent_a',
      edges=[('START', create_output_a)],
  )

  capture_node_b = InputCapturingNode(name='capture_node_b')
  nested_agent_b = Workflow(
      name='nested_agent_b',
      edges=[('START', capture_node_b)],
  )

  outer_agent = Workflow(
      name='outer_agent',
      edges=[
          ('START', nested_agent_a),
          (nested_agent_a, nested_agent_b),
      ],
  )

  app = App(
      name=request.function.__name__,
      root_agent=outer_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)
  await runner.run_async(testing_utils.get_user_content('hello'))

  assert capture_node_b.received_inputs == ['output of A']


@pytest.mark.asyncio
async def test_nested_workflow_with_tool_calls(
    request: pytest.FixtureRequest,
):
  """Tests that a nested Workflow works correctly with two tool calls."""
  tool_call_count = 0

  def simple_tool() -> str:
    nonlocal tool_call_count
    tool_call_count += 1
    return f'Tool output {tool_call_count}'

  # Given: A nested workflow where the inner node calls a tool
  llm_agent = LlmAgent(
      name='llm_agent',
      model=testing_utils.MockModel.create(
          responses=[
              types.Part.from_function_call(
                  name='simple_tool',
                  args={},
              ),
              types.Part.from_text(text='LLM response after tools'),
          ]
      ),
      tools=[simple_tool],
  )

  nested_agent = Workflow(
      name='nested_agent',
      edges=[('START', llm_agent)],
  )

  async def output_func():
    return 'I am outer'

  outer_agent = Workflow(
      name='outer_agent',
      edges=[
          ('START', nested_agent),
          (nested_agent, output_func),
      ],
  )

  app = App(
      name=request.function.__name__,
      root_agent=outer_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)

  # When: Starting the workflow
  user_event = testing_utils.get_user_content('start workflow')
  events = await runner.run_async(user_event)

  # Then: It should call the tool and yield events
  assert tool_call_count == 1

  simplified_events = simplify_events_with_node(events)

  # Extract the dynamically generated function_call_id from events.
  ev = find_function_call_event(events, 'simple_tool')
  assert ev is not None
  function_call_id = ev.content.parts[0].function_call.id

  assert simplified_events == [
      (
          'outer_agent@1/nested_agent@1/llm_agent@1',
          types.Part.from_function_call(name='simple_tool', args={}),
      ),
      (
          'outer_agent@1/nested_agent@1/llm_agent@1',
          types.Part(
              function_response=types.FunctionResponse(
                  name='simple_tool',
                  response={'result': 'Tool output 1'},
              )
          ),
      ),
      ('outer_agent@1/nested_agent@1/llm_agent@1', 'LLM response after tools'),
      (
          'outer_agent@1/output_func@1',
          {
              'output': 'I am outer',
          },
      ),
  ]


@pytest.mark.asyncio
async def test_three_level_nested_workflow(request: pytest.FixtureRequest):
  """Tests a 3-level nested Workflow structure."""

  async def inner_func():
    return 'I am inner'

  inner_agent = Workflow(
      name='inner_agent',
      edges=[('START', inner_func)],
  )

  async def middle_func():
    return 'I am middle'

  middle_agent = Workflow(
      name='middle_agent',
      edges=[('START', inner_agent), (inner_agent, middle_func)],
  )

  async def outer_func():
    return 'I am outer'

  outer_agent = Workflow(
      name='outer_agent',
      edges=[('START', middle_agent), (middle_agent, outer_func)],
  )

  app = App(
      name=request.function.__name__,
      root_agent=outer_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)
  events = await runner.run_async(testing_utils.get_user_content('hello'))

  simplified_events = simplify_events_with_node(events)
  assert simplified_events == [
      (
          'outer_agent@1/middle_agent@1/inner_agent@1/inner_func@1',
          {
              'output': 'I am inner',
          },
      ),
      (
          'outer_agent@1/middle_agent@1/middle_func@1',
          {
              'output': 'I am middle',
          },
      ),
      (
          'outer_agent@1/outer_func@1',
          {
              'output': 'I am outer',
          },
      ),
  ]


@pytest.mark.asyncio
async def test_duplicate_grandchild_workflow_names(
    request: pytest.FixtureRequest,
):
  """Tests that grandchild workflow agents with same name can coexist."""

  # Given: A workflow hierarchy where grandchild workflows have the same name
  async def grandchild_func():
    return 'I am grandchild'

  grandchild_agent = Workflow(
      name='grandchild',
      edges=[('START', grandchild_func)],
  )

  child1_agent = Workflow(
      name='child1',
      edges=[('START', copy.deepcopy(grandchild_agent))],
  )

  child2_agent = Workflow(
      name='child2',
      edges=[('START', copy.deepcopy(grandchild_agent))],
  )

  root_agent = Workflow(
      name='root',
      edges=[('START', child1_agent), (child1_agent, child2_agent)],
  )

  app = App(
      name=request.function.__name__,
      root_agent=root_agent,
  )
  runner = testing_utils.InMemoryRunner(app=app)
  # When: Starting the workflow
  user_content = testing_utils.get_user_content('hello')
  events = await runner.run_async(user_content)

  # Then: It should execute both grandchildren successfully
  simplified_events = simplify_events_with_node(events)
  assert simplified_events == [
      (
          'root@1/child1@1/grandchild@1/grandchild_func@1',
          {'output': 'I am grandchild'},
      ),
      (
          'root@1/child2@1/grandchild@1/grandchild_func@1',
          {'output': 'I am grandchild'},
      ),
  ]


@pytest.mark.asyncio
async def test_duplicate_name_in_ancestral_path(
    request: pytest.FixtureRequest,
):
  """Tests that agent with same name can exist in ancestral path (A->B->A)."""

  async def func_a():
    return 'I am A'

  agent_a_child = Workflow(
      name='A',
      edges=[('START', func_a)],
  )

  agent_b = Workflow(
      name='B',
      edges=[('START', agent_a_child)],
  )

  agent_a_root = Workflow(
      name='A',
      edges=[('START', agent_b)],
  )

  app = App(
      name=request.function.__name__,
      root_agent=agent_a_root,
  )
  runner = testing_utils.InMemoryRunner(app=app)
  user_content = testing_utils.get_user_content('hello')
  events = await runner.run_async(user_content)

  simplified_events = simplify_events_with_node(events)
  assert simplified_events == [
      (
          'A@1/B@1/A@1/func_a@1',
          {'output': 'I am A'},
      ),
  ]


# --- Helpers moved from test_workflow.py ---


class _OutputNode(BaseNode):
  """Yields a fixed output value."""

  value: Any = None

  async def _run_impl(
      self, *, ctx: Context, node_input: Any
  ) -> AsyncGenerator[Any, None]:
    yield self.value


class _InputCapturingNode(BaseNode):
  """Captures node_input for later assertion."""

  model_config = ConfigDict(arbitrary_types_allowed=True)
  received_inputs: list[Any] = Field(default_factory=list)

  async def _run_impl(
      self, *, ctx: Context, node_input: Any
  ) -> AsyncGenerator[Any, None]:
    self.received_inputs.append(node_input)
    yield {'received': node_input}


async def _run_workflow(wf, message='start'):
  """Run a Workflow through Runner, return collected events."""
  ss = InMemorySessionService()
  runner = Runner(app_name='test', node=wf, session_service=ss)
  session = await ss.create_session(app_name='test', user_id='u')
  msg = types.Content(parts=[types.Part(text=message)], role='user')
  events = []
  async for event in runner.run_async(
      user_id='u', session_id=session.id, new_message=msg
  ):
    events.append(event)
  return events, ss, session


def _outputs(events):
  """Extract non-None outputs from events."""
  return [e.output for e in events if e.output is not None]


def _output_by_node(events):
  """Extract (node_name_from_path, output) for child node events."""
  results = []
  for e in events:
    if e.output is not None and e.node_info.path and '/' in e.node_info.path:
      node_name = e.node_info.path.rsplit('/', 1)[-1]
      if '@' in node_name:
        node_name = node_name.rsplit('@', 1)[0]
      results.append((node_name, e.output))
  return results


# --- Tests moved from test_workflow.py ---


@pytest.mark.asyncio
async def test_nested_workflow_completes():
  """Inner workflow runs to completion, outer continues downstream."""
  inner_node = _OutputNode(name='inner_node', value='inner_result')
  inner_wf = Workflow(name='inner_wf', edges=[(START, inner_node)])
  before = _OutputNode(name='before', value='before_result')
  after = _InputCapturingNode(name='after')
  wf = Workflow(name='wf', edges=[(START, before, inner_wf, after)])

  events, _, _ = await _run_workflow(wf)

  by_node = _output_by_node(events)
  assert ('before', 'before_result') in by_node
  assert ('inner_node', 'inner_result') in by_node
  assert after.received_inputs == ['inner_result']


@pytest.mark.asyncio
async def test_nested_workflow_event_author():
  """Events are authored by the nearest orchestrator (workflow/agent).

  Setup: outer_wf → inner_wf → inner_node.
  Assert:
    - inner_node's events are authored by inner_wf (nearest).
    - outer_wf's direct children are authored by outer_wf.
    - inner_wf overrides the author for its subtree.
  """
  inner_node = _OutputNode(name='inner_node', value='inner_result')
  inner_wf = Workflow(name='inner_wf', edges=[(START, inner_node)])
  outer_node = _OutputNode(name='outer_node', value='outer_result')
  wf = Workflow(
      name='outer_wf',
      edges=[(START, outer_node, inner_wf)],
  )

  events, _, _ = await _run_workflow(wf)

  # outer_node's events authored by outer_wf (nearest orchestrator).
  outer_events = [
      e for e in events if e.node_info.path == 'outer_wf@1/outer_node@1'
  ]
  assert outer_events
  assert all(e.author == 'outer_wf' for e in outer_events)

  # inner_node's events authored by inner_wf (nearest orchestrator),
  # NOT outer_wf.
  inner_events = [
      e
      for e in events
      if e.node_info.path == 'outer_wf@1/inner_wf@1/inner_node@1'
  ]
  assert inner_events
  assert all(e.author == 'inner_wf' for e in inner_events)


@pytest.mark.asyncio
async def test_nested_workflow_interrupt_and_resume():
  """Inner workflow child interrupts, outer resumes on FR."""

  class _InterruptNode(BaseNode):
    rerun_on_resume: bool = True

    async def _run_impl(
        self, *, ctx: Context, node_input: Any
    ) -> AsyncGenerator[Any, None]:
      fc_id = ctx.state.get('_nested_fc')
      if fc_id and ctx.resume_inputs and fc_id in ctx.resume_inputs:
        ctx.state['_nested_fc'] = None
        response = ctx.resume_inputs[fc_id]['answer']
        yield f'approved:{response}'
        return
      fc_id = f'fc-{uuid.uuid4().hex[:8]}'
      ctx.state['_nested_fc'] = fc_id
      yield Event(
          content=types.Content(
              parts=[
                  types.Part(
                      function_call=types.FunctionCall(
                          name='inner_tool', args={}, id=fc_id
                      )
                  )
              ]
          ),
          long_running_tool_ids={fc_id},
      )

  inner_wf = Workflow(
      name='inner_wf',
      edges=[(START, _InterruptNode(name='approval'))],
  )
  before = _OutputNode(name='before', value='before_result')
  after = _InputCapturingNode(name='after')
  wf = Workflow(
      name='wf',
      edges=[(START, before, inner_wf, after)],
  )
  ss = InMemorySessionService()
  runner = Runner(app_name='test', node=wf, session_service=ss)
  session = await ss.create_session(app_name='test', user_id='u')

  # Run 1: before completes, inner_wf/approval interrupts
  msg1 = types.Content(parts=[types.Part(text='go')], role='user')
  events1: list[Event] = []
  async for event in runner.run_async(
      user_id='u', session_id=session.id, new_message=msg1
  ):
    events1.append(event)

  # Should have interrupt from inner's child
  interrupt_events = [e for e in events1 if e.long_running_tool_ids]
  assert len(interrupt_events) == 1
  assert interrupt_events[0].long_running_tool_ids is not None
  fc_id = list(interrupt_events[0].long_running_tool_ids)[0]

  # Workflow-level interrupt events should NOT be persisted
  # (they're _adk_internal). Only the leaf child's event at
  # 'wf/inner_wf/approval' should have interrupt ids in session.
  updated_session = await ss.get_session(
      app_name='test', user_id='u', session_id=session.id
  )
  assert updated_session is not None
  wf_interrupt_events = [
      e
      for e in updated_session.events
      if e.long_running_tool_ids and e.node_info.path in ('wf', 'wf/inner_wf')
  ]
  assert wf_interrupt_events == []

  # Run 2: resume
  msg2 = types.Content(
      parts=[
          types.Part(
              function_response=types.FunctionResponse(
                  name='inner_tool',
                  id=fc_id,
                  response={'answer': 'yes'},
              )
          )
      ],
      role='user',
  )
  events2: list[Event] = []
  async for event in runner.run_async(
      user_id='u', session_id=session.id, new_message=msg2
  ):
    events2.append(event)

  # Inner resumed, after should receive inner's output
  outputs = [e.output for e in events2 if e.output is not None]
  assert 'approved:yes' in outputs
  assert after.received_inputs == ['approved:yes']


@pytest.mark.asyncio
async def test_nested_workflow_partial_resume():
  """Partial FR re-runs nested Workflow, resolved child completes while unresolved stays interrupted.

  Setup: outer_wf → inner_wf → (child_a, child_b) → join.
    Both children interrupt on first run.
  Act:
    - Run 2: resolve only child_a's FR.
    - Run 3: resolve child_b's FR.
  Assert:
    - Run 2: child_a produces output, invocation still interrupted.
    - Run 3: child_b produces output, join completes, no interrupts.
  """

  class _InterruptOnce(BaseNode):
    """Interrupts on first run, yields resume response on second."""

    rerun_on_resume: bool = True

    async def _run_impl(
        self, *, ctx: Context, node_input: Any
    ) -> AsyncGenerator[Any, None]:
      fc_id = f'fc-{self.name}'
      if ctx.resume_inputs and fc_id in ctx.resume_inputs:
        yield f'{self.name}:{ctx.resume_inputs[fc_id]}'
        return
      yield RequestInput(interrupt_id=fc_id)

  child_a = _InterruptOnce(name='child_a')
  child_b = _InterruptOnce(name='child_b')
  join = JoinNode(name='join', wait_for_output=True)

  inner_wf = Workflow(
      name='inner_wf',
      edges=[
          (START, child_a),
          (START, child_b),
          (child_a, join),
          (child_b, join),
      ],
  )

  outer_wf = Workflow(
      name='outer',
      edges=[(START, inner_wf)],
  )

  ss = InMemorySessionService()
  runner = Runner(app_name='test', node=outer_wf, session_service=ss)
  session = await ss.create_session(app_name='test', user_id='u')

  # Run 1: both children interrupt
  msg1 = types.Content(parts=[types.Part(text='go')], role='user')
  events1: list[Event] = []
  async for event in runner.run_async(
      user_id='u', session_id=session.id, new_message=msg1
  ):
    events1.append(event)

  interrupt_ids = set()
  for e in events1:
    if e.long_running_tool_ids:
      interrupt_ids.update(e.long_running_tool_ids)
  assert 'fc-child_a' in interrupt_ids
  assert 'fc-child_b' in interrupt_ids

  # Run 2: resolve only child_a
  msg2 = types.Content(
      parts=[create_request_input_response('fc-child_a', {'v': 'a'})],
      role='user',
  )
  events2: list[Event] = []
  async for event in runner.run_async(
      user_id='u', session_id=session.id, new_message=msg2
  ):
    events2.append(event)

  # child_a should have produced output
  child_a_outputs = [
      e.output
      for e in events2
      if e.node_info.path and 'child_a' in e.node_info.path and e.output
  ]
  assert any('child_a:' in str(o) for o in child_a_outputs)

  # Run 3: resolve child_b → join completes, workflow finishes
  msg3 = types.Content(
      parts=[create_request_input_response('fc-child_b', {'v': 'b'})],
      role='user',
  )
  events3: list[Event] = []
  async for event in runner.run_async(
      user_id='u', session_id=session.id, new_message=msg3
  ):
    events3.append(event)

  # child_b should have produced output
  child_b_outputs = [
      e.output
      for e in events3
      if e.node_info.path and 'child_b' in e.node_info.path and e.output
  ]
  assert any('child_b:' in str(o) for o in child_b_outputs)

  # join should have completed (no more interrupts)
  final_interrupts = set()
  for e in events3:
    if e.long_running_tool_ids:
      final_interrupts.update(e.long_running_tool_ids)
  assert not final_interrupts
