"""Async tests for time travel (replay and fork) behavior.

Covers the intersection of replay vs fork across graph structures:
- Replay & fork basics (no interrupt, no subgraph)
- Replay & fork with interrupts (no subgraph)
- Multiple / sequential interrupts
- Subgraph without interrupt
- Subgraph with interrupt
- __copy__ / update_state(None)
- Observability (get_state, config access)

Key concepts:
- Replay (invoke with checkpoint_id): Re-executes nodes after the checkpoint.
  Interrupts re-fire on replay.
- Fork (update_state then invoke): Creates a new checkpoint without cached
  pending writes. Nodes re-execute and interrupts DO re-fire.
"""

import operator
import sys
from typing import Annotated

import pytest
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import BaseCheckpointSaver
from typing_extensions import TypedDict

from langgraph.graph import START, StateGraph
from langgraph.types import Command, interrupt

pytestmark = pytest.mark.anyio

NEEDS_CONTEXTVARS = pytest.mark.skipif(
    sys.version_info < (3, 11),
    reason="Python 3.11+ is required for async contextvars support",
)


class State(TypedDict):
    value: Annotated[list[str], operator.add]


def _checkpoint_summary(history: list) -> list[dict]:
    """Summarize checkpoint history into a readable format for assertions.

    Returns a list of dicts (newest-first, matching get_state_history order) with:
      - id: short checkpoint id suffix (last 6 chars)
      - parent_id: short parent checkpoint id suffix or None
      - source: checkpoint metadata source (input, loop, fork, update)
      - next: tuple of next node names
      - values: channel values snapshot
    """
    summaries = []
    for s in history:
        cid = s.config["configurable"]["checkpoint_id"]
        pid = (
            s.parent_config["configurable"]["checkpoint_id"]
            if s.parent_config
            else None
        )
        summaries.append(
            {
                "id": cid[-6:],
                "parent_id": pid[-6:] if pid else None,
                "source": s.metadata.get("source"),
                "next": s.next,
                "values": s.values,
            }
        )
    return summaries


# ---------------------------------------------------------------------------
# Section 1: Replay & fork basics (no interrupt, no subgraph)
# ---------------------------------------------------------------------------


@NEEDS_CONTEXTVARS
async def test_replay_reruns_nodes_after_checkpoint(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replay from checkpoint before node_b. node_b re-executes (it's after
    the checkpoint), node_a does not."""

    called: list[str] = []

    def node_a(state: State) -> State:
        called.append("node_a")
        return {"value": ["a"]}

    def node_b(state: State) -> State:
        called.append("node_b")
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    result = await graph.ainvoke({"value": []}, config)
    assert result == {"value": ["a", "b"]}
    assert called == ["node_a", "node_b"]

    # Find checkpoint before node_b (next=(node_b,))
    history = [s async for s in graph.aget_state_history(config)]
    before_b = next(s for s in history if s.next == ("node_b",))

    # Replay from checkpoint before node_b
    called.clear()
    replay_result = await graph.ainvoke(None, before_b.config)

    assert replay_result == {"value": ["a", "b"]}
    assert "node_b" in called
    assert "node_a" not in called


@NEEDS_CONTEXTVARS
async def test_replay_from_final_checkpoint_is_noop(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replay from completed checkpoint (no next nodes) is a no-op."""

    called: list[str] = []

    def node_a(state: State) -> State:
        called.append("node_a")
        return {"value": ["a"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_edge(START, "node_a")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    result = await graph.ainvoke({"value": []}, config)
    assert result == {"value": ["a"]}

    state = await graph.aget_state(config)
    assert state.next == ()

    called.clear()
    replay_result = await graph.ainvoke(None, state.config)
    assert replay_result == {"value": ["a"]}
    assert called == []


@NEEDS_CONTEXTVARS
async def test_fork_reruns_with_modified_state(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Fork from checkpoint before node_b with modified state. node_b
    re-executes with the new state."""

    called: list[str] = []

    def node_a(state: State) -> State:
        called.append("node_a")
        return {"value": ["a"]}

    def node_b(state: State) -> State:
        called.append("node_b")
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    await graph.ainvoke({"value": []}, config)

    history = [s async for s in graph.aget_state_history(config)]
    before_b = next(s for s in history if s.next == ("node_b",))

    called.clear()
    fork_config = await graph.aupdate_state(before_b.config, {"value": ["x"]})
    fork_result = await graph.ainvoke(None, fork_config)

    assert "node_b" in called
    assert fork_result == {"value": ["a", "x", "b"]}


@NEEDS_CONTEXTVARS
async def test_multiple_forks_from_same_checkpoint(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Two independent forks from the same checkpoint create independent
    branches that don't affect each other."""

    def node_a(state: State) -> State:
        return {"value": ["a"]}

    def node_b(state: State) -> State:
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    await graph.ainvoke({"value": []}, config)

    history = [s async for s in graph.aget_state_history(config)]
    before_b = next(s for s in history if s.next == ("node_b",))

    fork1_config = await graph.aupdate_state(before_b.config, {"value": ["fork1"]})
    result1 = await graph.ainvoke(None, fork1_config)

    fork2_config = await graph.aupdate_state(before_b.config, {"value": ["fork2"]})
    result2 = await graph.ainvoke(None, fork2_config)

    assert "fork1" in result1["value"]
    assert "fork2" not in result1["value"]
    assert "fork2" in result2["value"]
    assert "fork1" not in result2["value"]


# ---------------------------------------------------------------------------
# Section 2: Replay & fork with interrupts (no subgraph)
# ---------------------------------------------------------------------------


@NEEDS_CONTEXTVARS
async def test_replay_from_before_interrupt_refires(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replay from checkpoint before interrupt node. The node re-executes
    and interrupt re-fires."""

    call_count: dict[str, int] = {"node_a": 0, "ask_human": 0, "node_b": 0}

    def node_a(state: State) -> State:
        call_count["node_a"] += 1
        return {"value": ["a"]}

    def ask_human(state: State) -> State:
        call_count["ask_human"] += 1
        answer = interrupt("What is your input?")
        return {"value": [f"human:{answer}"]}

    def node_b(state: State) -> State:
        call_count["node_b"] += 1
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("ask_human", ask_human)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "ask_human")
        .add_edge("ask_human", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Run until interrupt
    result = await graph.ainvoke({"value": []}, config)
    assert "__interrupt__" in result
    assert call_count["ask_human"] == 1

    # Resume
    result = await graph.ainvoke(Command(resume="hello"), config)
    assert result == {"value": ["a", "human:hello", "b"]}
    assert call_count["ask_human"] == 2  # re-executes on resume

    # Find checkpoint before ask_human
    history = [s async for s in graph.aget_state_history(config)]
    before_ask = [s for s in history if s.next == ("ask_human",)][-1]

    # Replay — interrupt re-fires, node re-executes
    replay_result = await graph.ainvoke(None, before_ask.config)

    assert "__interrupt__" in replay_result
    assert replay_result["__interrupt__"][0].value == "What is your input?"
    assert replay_result["value"] == ["a"]
    assert call_count["ask_human"] == 3  # re-executed again
    assert call_count["node_a"] == 1  # NOT re-executed (before checkpoint)
    assert call_count["node_b"] == 1  # NOT re-executed (after interrupt)


@NEEDS_CONTEXTVARS
async def test_replay_interrupt_stable_across_replays(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replaying the same checkpoint multiple times consistently produces
    identical results (interrupt re-fires each time)."""

    def node_a(state: State) -> State:
        return {"value": ["a"]}

    def ask_human(state: State) -> State:
        answer = interrupt("What is your input?")
        return {"value": [f"human:{answer}"]}

    def node_b(state: State) -> State:
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("ask_human", ask_human)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "ask_human")
        .add_edge("ask_human", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    await graph.ainvoke({"value": []}, config)
    await graph.ainvoke(Command(resume="cached_answer"), config)

    history = [s async for s in graph.aget_state_history(config)]
    before_ask = [s for s in history if s.next == ("ask_human",)][-1]

    results = []
    for _ in range(3):
        r = await graph.ainvoke(None, before_ask.config)
        results.append(r)

    # Each replay creates a fork with a unique interrupt ID, so we compare
    # interrupt values and state values rather than full equality.
    assert all("__interrupt__" in r for r in results)
    assert all(
        r["__interrupt__"][0].value == results[0]["__interrupt__"][0].value
        for r in results
    )
    assert all(r["value"] == results[0]["value"] for r in results)


@NEEDS_CONTEXTVARS
async def test_fork_from_before_interrupt_refires(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Fork from checkpoint before interrupt node. Interrupt IS re-triggered
    because fork has no cached resume values. Resume with new answer."""

    called: list[str] = []

    def node_a(state: State) -> State:
        called.append("node_a")
        return {"value": ["a"]}

    def ask_human(state: State) -> State:
        called.append("ask_human")
        answer = interrupt("What is your input?")
        return {"value": [f"human:{answer}"]}

    def node_b(state: State) -> State:
        called.append("node_b")
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("ask_human", ask_human)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "ask_human")
        .add_edge("ask_human", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    await graph.ainvoke({"value": []}, config)
    await graph.ainvoke(Command(resume="hello"), config)

    history = [s async for s in graph.aget_state_history(config)]
    before_ask = [s for s in history if s.next == ("ask_human",)][-1]

    called.clear()
    fork_config = await graph.aupdate_state(before_ask.config, {"value": ["forked"]})
    fork_result = await graph.ainvoke(None, fork_config)

    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "What is your input?"

    # Resume the forked interrupt with a different answer
    final = await graph.ainvoke(Command(resume="world"), fork_config)
    assert final == {"value": ["a", "forked", "human:world", "b"]}


@NEEDS_CONTEXTVARS
async def test_fork_from_interrupt_checkpoint_refires(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Fork from the checkpoint where interrupt fired. Interrupt re-triggered
    because fork clears cached data. Resume with different answer."""

    called: list[str] = []

    def node_a(state: State) -> State:
        called.append("node_a")
        return {"value": ["a"]}

    def ask_human(state: State) -> State:
        called.append("ask_human")
        answer = interrupt("What is your input?")
        return {"value": [f"human:{answer}"]}

    def node_b(state: State) -> State:
        called.append("node_b")
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("ask_human", ask_human)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "ask_human")
        .add_edge("ask_human", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    await graph.ainvoke({"value": []}, config)
    await graph.ainvoke(Command(resume="hello"), config)

    history = [s async for s in graph.aget_state_history(config)]
    interrupt_checkpoint = next(
        s
        for s in history
        if s.next == ("ask_human",) and s.tasks and any(t.interrupts for t in s.tasks)
    )

    fork_config = await graph.aupdate_state(
        interrupt_checkpoint.config, {"value": ["forked"]}
    )
    fork_result = await graph.ainvoke(None, fork_config)

    assert "__interrupt__" in fork_result

    final = await graph.ainvoke(Command(resume="different"), fork_config)
    assert "human:different" in final["value"]


# ---------------------------------------------------------------------------
# Section 3: Multiple / sequential interrupts
# ---------------------------------------------------------------------------


@NEEDS_CONTEXTVARS
async def test_sequential_interrupts_fork_from_middle(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Graph with two sequential interrupt nodes. Fork from between them:
    only the second re-fires, the first's result is preserved. Also verify
    replaying from before the first re-fires only the first."""

    called: list[str] = []

    def node_a(state: State) -> State:
        called.append("node_a")
        return {"value": ["a"]}

    def interrupt_1(state: State) -> State:
        called.append("interrupt_1")
        answer = interrupt("First question?")
        return {"value": [f"i1:{answer}"]}

    def interrupt_2(state: State) -> State:
        called.append("interrupt_2")
        answer = interrupt("Second question?")
        return {"value": [f"i2:{answer}"]}

    def node_b(state: State) -> State:
        called.append("node_b")
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("interrupt_1", interrupt_1)
        .add_node("interrupt_2", interrupt_2)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "interrupt_1")
        .add_edge("interrupt_1", "interrupt_2")
        .add_edge("interrupt_2", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Hit first interrupt
    r1 = await graph.ainvoke({"value": []}, config)
    assert r1["__interrupt__"][0].value == "First question?"

    # Resume first → hit second
    r2 = await graph.ainvoke(Command(resume="ans1"), config)
    assert r2["__interrupt__"][0].value == "Second question?"

    # Resume second → complete
    r3 = await graph.ainvoke(Command(resume="ans2"), config)
    assert r3 == {"value": ["a", "i1:ans1", "i2:ans2", "b"]}

    history = [s async for s in graph.aget_state_history(config)]

    # Fork from between the two interrupts — only second re-fires
    between = [s for s in history if s.next == ("interrupt_2",)][-1]
    fork_config = await graph.aupdate_state(between.config, {"value": ["mid_fork"]})
    fork_result = await graph.ainvoke(None, fork_config)

    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "Second question?"

    # Resume with new answer, first answer preserved
    final_result = await graph.ainvoke(Command(resume="new_b"), fork_config)
    assert "i2:new_b" in final_result["value"]
    assert "i1:ans1" in final_result["value"]

    # Replay from before first interrupt — first re-fires, second does not
    before_i1 = [s for s in history if s.next == ("interrupt_1",)][-1]
    called.clear()
    replay_result = await graph.ainvoke(None, before_i1.config)
    assert "__interrupt__" in replay_result
    assert replay_result["__interrupt__"][0].value == "First question?"
    assert "interrupt_1" in called
    assert "interrupt_2" not in called


@NEEDS_CONTEXTVARS
async def test_multiple_interrupts_in_one_node(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """A single node with two sequential interrupt() calls. Resuming resolves
    them one at a time. Replaying from before the node re-fires the first."""

    def multi_interrupt_node(state: State) -> State:
        answer1 = interrupt("First question?")
        answer2 = interrupt("Second question?")
        return {"value": [f"a1:{answer1}", f"a2:{answer2}"]}

    def after(state: State) -> State:
        return {"value": ["done"]}

    graph = (
        StateGraph(State)
        .add_node("ask", multi_interrupt_node)
        .add_node("after", after)
        .add_edge(START, "ask")
        .add_edge("ask", "after")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Hit first interrupt
    result = await graph.ainvoke({"value": []}, config)
    assert result["__interrupt__"][0].value == "First question?"

    # Resume first → hit second
    interrupt_state = await graph.aget_state(config)
    result = await graph.ainvoke(Command(resume="ans1"), interrupt_state.config)
    assert result["__interrupt__"][0].value == "Second question?"

    # Resume second → complete
    interrupt_state2 = await graph.aget_state(config)
    result = await graph.ainvoke(Command(resume="ans2"), interrupt_state2.config)
    assert result == {"value": ["a1:ans1", "a2:ans2", "done"]}

    # Replay from before the node — first interrupt re-fires
    history = [s async for s in graph.aget_state_history(config)]
    before_ask = [s for s in history if s.next == ("ask",)][-1]
    replay_result = await graph.ainvoke(None, before_ask.config)
    assert replay_result["__interrupt__"][0].value == "First question?"


# ---------------------------------------------------------------------------
# Section 4: Subgraph without interrupt
# ---------------------------------------------------------------------------


@NEEDS_CONTEXTVARS
async def test_subgraph_replay_from_before(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replay from parent checkpoint before subgraph node. Subgraph and
    post_process re-execute, parent_node does not."""

    called: list[str] = []

    def parent_node(state: State) -> State:
        called.append("parent_node")
        return {"value": ["parent"]}

    def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["sub_a"]}

    def step_b(state: State) -> State:
        called.append("step_b")
        return {"value": ["sub_b"]}

    subgraph = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("step_b", step_b)
        .add_edge(START, "step_a")
        .add_edge("step_a", "step_b")
        .compile()
    )

    def post_process(state: State) -> State:
        called.append("post_process")
        return {"value": ["post"]}

    graph = (
        StateGraph(State)
        .add_node("parent_node", parent_node)
        .add_node("subgraph", subgraph)
        .add_node("post_process", post_process)
        .add_edge(START, "parent_node")
        .add_edge("parent_node", "subgraph")
        .add_edge("subgraph", "post_process")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    result = await graph.ainvoke({"value": []}, config)
    assert "sub_a" in result["value"]
    assert "sub_b" in result["value"]
    assert "post" in result["value"]

    history = [s async for s in graph.aget_state_history(config)]
    before_sub = next(s for s in history if s.next == ("subgraph",))

    called.clear()
    replay_result = await graph.ainvoke(None, before_sub.config)

    assert "sub_a" in replay_result["value"]
    assert "sub_b" in replay_result["value"]
    assert "post" in replay_result["value"]
    assert "parent_node" not in called


@NEEDS_CONTEXTVARS
async def test_subgraph_fork_from_before(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Fork from parent checkpoint before subgraph with modified state.
    Subgraph re-executes with forked state."""

    called: list[str] = []

    def parent_node(state: State) -> State:
        called.append("parent_node")
        return {"value": ["parent"]}

    def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["sub_a"]}

    def step_b(state: State) -> State:
        called.append("step_b")
        return {"value": ["sub_b"]}

    subgraph = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("step_b", step_b)
        .add_edge(START, "step_a")
        .add_edge("step_a", "step_b")
        .compile()
    )

    def post_process(state: State) -> State:
        called.append("post_process")
        return {"value": ["post"]}

    graph = (
        StateGraph(State)
        .add_node("parent_node", parent_node)
        .add_node("subgraph", subgraph)
        .add_node("post_process", post_process)
        .add_edge(START, "parent_node")
        .add_edge("parent_node", "subgraph")
        .add_edge("subgraph", "post_process")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    await graph.ainvoke({"value": []}, config)

    history = [s async for s in graph.aget_state_history(config)]
    before_sub = next(s for s in history if s.next == ("subgraph",))

    called.clear()
    fork_config = await graph.aupdate_state(before_sub.config, {"value": ["forked"]})
    fork_result = await graph.ainvoke(None, fork_config)

    assert "step_a" in called
    assert "step_b" in called
    assert "post_process" in called
    assert "forked" in fork_result["value"]
    assert "sub_a" in fork_result["value"]
    assert "sub_b" in fork_result["value"]


# ---------------------------------------------------------------------------
# Section 5: Subgraph with interrupt
# ---------------------------------------------------------------------------


@NEEDS_CONTEXTVARS
async def test_subgraph_interrupt_replay_from_parent(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replay from parent checkpoint before subgraph. Subgraph re-executes
    and interrupt re-fires."""

    called: list[str] = []

    def router(state: State) -> State:
        called.append("router")
        return {"value": ["routed"]}

    def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["sub_a"]}

    def ask_human(state: State) -> State:
        called.append("ask_human")
        answer = interrupt("Provide input:")
        return {"value": [f"human:{answer}"]}

    def step_b(state: State) -> State:
        called.append("step_b")
        return {"value": ["sub_b"]}

    subgraph = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_human", ask_human)
        .add_node("step_b", step_b)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_human")
        .add_edge("ask_human", "step_b")
        .compile(checkpointer=True)
    )

    def post_process(state: State) -> State:
        called.append("post_process")
        return {"value": ["post"]}

    graph = (
        StateGraph(State)
        .add_node("router", router)
        .add_node("subgraph_node", subgraph)
        .add_node("post_process", post_process)
        .add_edge(START, "router")
        .add_edge("router", "subgraph_node")
        .add_edge("subgraph_node", "post_process")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Run until interrupt, then resume
    result = await graph.ainvoke({"value": []}, config)
    assert "__interrupt__" in result

    completed_result = await graph.ainvoke(Command(resume="answer"), config)
    assert "human:answer" in completed_result["value"]
    assert "sub_b" in completed_result["value"]
    assert "post" in completed_result["value"]

    # Find parent checkpoint before subgraph_node
    history = [s async for s in graph.aget_state_history(config)]
    before_sub = [s for s in history if s.next == ("subgraph_node",)][-1]

    # Replay — interrupt re-fires
    called.clear()
    replay_result = await graph.ainvoke(None, before_sub.config)
    assert "__interrupt__" in replay_result


@NEEDS_CONTEXTVARS
async def test_subgraph_interrupt_replay_from_interrupt_checkpoint(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replay from the parent checkpoint where subgraph interrupt fired.
    Interrupt re-fires."""

    called: list[str] = []

    def router(state: State) -> State:
        called.append("router")
        return {"value": ["routed"]}

    def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["sub_a"]}

    def ask_human(state: State) -> State:
        called.append("ask_human")
        answer = interrupt("Provide input:")
        return {"value": [f"human:{answer}"]}

    def step_b(state: State) -> State:
        called.append("step_b")
        return {"value": ["sub_b"]}

    subgraph = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_human", ask_human)
        .add_node("step_b", step_b)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_human")
        .add_edge("ask_human", "step_b")
        .compile(checkpointer=True)
    )

    def post_process(state: State) -> State:
        called.append("post_process")
        return {"value": ["post"]}

    graph = (
        StateGraph(State)
        .add_node("router", router)
        .add_node("subgraph_node", subgraph)
        .add_node("post_process", post_process)
        .add_edge(START, "router")
        .add_edge("router", "subgraph_node")
        .add_edge("subgraph_node", "post_process")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Run until interrupt
    await graph.ainvoke({"value": []}, config)

    # Verify subgraph state is accessible
    parent_state = await graph.aget_state(config, subgraphs=True)
    assert len(parent_state.tasks) > 0
    assert parent_state.tasks[0].state is not None

    # Resume
    await graph.ainvoke(Command(resume="answer"), config)

    # Find the parent checkpoint where the interrupt fired
    history = [s async for s in graph.aget_state_history(config)]
    interrupt_checkpoint = next(
        s
        for s in history
        if s.next == ("subgraph_node",)
        and s.tasks
        and any(t.interrupts for t in s.tasks)
    )

    called.clear()
    replay_result = await graph.ainvoke(None, interrupt_checkpoint.config)
    assert "__interrupt__" in replay_result


@NEEDS_CONTEXTVARS
async def test_subgraph_interrupt_full_flow(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Fork from the subgraph's own checkpoint, resume the interrupt with a
    new answer, and verify the FULL flow: subgraph completes (step_b runs)
    AND execution continues back to the parent graph (post_process runs).

    This is the key test for time-traveling to a subgraph checkpoint,
    re-triggering the interrupt, providing a new answer, and having the
    entire graph complete normally including parent nodes after the subgraph."""

    called: list[str] = []

    def router(state: State) -> State:
        called.append("router")
        return {"value": ["routed"]}

    def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["sub_a"]}

    def ask_human(state: State) -> State:
        called.append("ask_human")
        answer = interrupt("Provide input:")
        return {"value": [f"human:{answer}"]}

    def step_b(state: State) -> State:
        called.append("step_b")
        return {"value": ["sub_b"]}

    subgraph = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_human", ask_human)
        .add_node("step_b", step_b)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_human")
        .add_edge("ask_human", "step_b")
        .compile(checkpointer=True)
    )

    def post_process(state: State) -> State:
        called.append("post_process")
        return {"value": ["post"]}

    graph = (
        StateGraph(State)
        .add_node("router", router)
        .add_node("subgraph_node", subgraph)
        .add_node("post_process", post_process)
        .add_edge(START, "router")
        .add_edge("router", "subgraph_node")
        .add_edge("subgraph_node", "post_process")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Run until interrupt fires in subgraph
    result = await graph.ainvoke({"value": []}, config)
    assert "__interrupt__" in result
    assert "router" in called
    assert "step_a" in called
    assert "ask_human" in called

    # Get subgraph's own checkpoint config
    parent_state = await graph.aget_state(config, subgraphs=True)
    sub_task = parent_state.tasks[0]
    assert sub_task.state is not None
    sub_config = sub_task.state.config

    # Fork from subgraph checkpoint
    called.clear()
    fork_config = await graph.aupdate_state(sub_config, {"value": ["sub_forked"]})

    # Invoke from fork — interrupt should re-fire
    fork_result = await graph.ainvoke(None, fork_config)
    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "Provide input:"

    # Resume the re-triggered interrupt with a NEW answer
    called.clear()
    final_result = await graph.ainvoke(Command(resume="new_answer"), fork_config)

    # Verify full completion
    assert "ask_human" in called
    assert "human:new_answer" in final_result["value"]
    assert "step_b" in called
    assert "sub_b" in final_result["value"]
    assert "post_process" in called
    assert "post" in final_result["value"]


@NEEDS_CONTEXTVARS
async def test_subgraph_interrupt_full_flow_no_sub_checkpointer(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Same as test_subgraph_interrupt_full_flow but with no sub-checkpointer
    (checkpointer=None). Fork from the parent checkpoint before the subgraph,
    re-trigger interrupt, resume, and verify full parent completion."""

    called: list[str] = []

    def router(state: State) -> State:
        called.append("router")
        return {"value": ["routed"]}

    def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["sub_a"]}

    def ask_human(state: State) -> State:
        called.append("ask_human")
        answer = interrupt("Provide input:")
        return {"value": [f"human:{answer}"]}

    def step_b(state: State) -> State:
        called.append("step_b")
        return {"value": ["sub_b"]}

    subgraph = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_human", ask_human)
        .add_node("step_b", step_b)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_human")
        .add_edge("ask_human", "step_b")
        .compile(checkpointer=None)
    )

    def post_process(state: State) -> State:
        called.append("post_process")
        return {"value": ["post"]}

    graph = (
        StateGraph(State)
        .add_node("router", router)
        .add_node("subgraph_node", subgraph)
        .add_node("post_process", post_process)
        .add_edge(START, "router")
        .add_edge("router", "subgraph_node")
        .add_edge("subgraph_node", "post_process")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Run until interrupt, then resume to complete
    result = await graph.ainvoke({"value": []}, config)
    assert "__interrupt__" in result

    original_result = await graph.ainvoke(Command(resume="original"), config)
    assert "human:original" in original_result["value"]
    assert "post" in original_result["value"]

    # Find parent checkpoint before subgraph_node
    history = [s async for s in graph.aget_state_history(config)]
    before_sub = [s for s in history if s.next == ("subgraph_node",)][-1]

    # Fork from parent checkpoint
    called.clear()
    fork_config = await graph.aupdate_state(before_sub.config, {"value": ["forked"]})
    fork_result = await graph.ainvoke(None, fork_config)

    # Interrupt IS re-triggered
    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "Provide input:"

    # Resume with new answer
    called.clear()
    final_result = await graph.ainvoke(Command(resume="new_answer"), fork_config)

    # Verify full completion back through parent
    assert "human:new_answer" in final_result["value"]
    assert "step_b" in called
    assert "sub_b" in final_result["value"]
    assert "post_process" in called
    assert "post" in final_result["value"]


@NEEDS_CONTEXTVARS
async def test_subgraph_time_travel_to_first_interrupt_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Time travel to a subgraph checkpoint at the FIRST interrupt (async)."""

    called: list[str] = []

    async def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["step_a_done"]}

    async def ask_1(state: State) -> State:
        called.append("ask_1")
        answer = interrupt("Question 1?")
        return {"value": [f"ask_1:{answer}"]}

    async def ask_2(state: State) -> State:
        called.append("ask_2")
        answer = interrupt("Question 2?")
        return {"value": [f"ask_2:{answer}"]}

    executor = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_1", ask_1)
        .add_node("ask_2", ask_2)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_1")
        .add_edge("ask_1", "ask_2")
        .add_edge("ask_2", "__end__")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(State)
        .add_node("executor", executor)
        .add_edge(START, "executor")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Run until first interrupt (ask_1)
    result = await graph.ainvoke({"value": []}, config)
    assert result["__interrupt__"][0].value == "Question 1?"

    # Capture subgraph state at the first interrupt
    parent_state = await graph.aget_state(config, subgraphs=True)
    sub_config_at_first = parent_state.tasks[0].state.config

    # Resume through both interrupts to complete
    await graph.ainvoke(Command(resume="answer_1"), config)
    await graph.ainvoke(Command(resume="answer_2"), config)

    # --- Scenario 1: Replay from subgraph checkpoint at 1st interrupt ---
    called.clear()
    replay_result = await graph.ainvoke(None, sub_config_at_first)
    assert "__interrupt__" in replay_result
    assert replay_result["__interrupt__"][0].value == "Question 1?"
    assert "step_a" not in called
    assert "ask_1" in called

    # --- Scenario 2: Fork from subgraph checkpoint at 1st interrupt ---
    called.clear()
    fork_config = await graph.aupdate_state(sub_config_at_first, {"value": ["forked"]})
    fork_result = await graph.ainvoke(None, fork_config)
    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "Question 1?"
    assert "step_a" not in called
    assert "ask_1" in called


@NEEDS_CONTEXTVARS
async def test_subgraph_time_travel_to_second_interrupt_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Time travel to a subgraph checkpoint at the SECOND interrupt (async)."""

    called: list[str] = []

    async def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["step_a_done"]}

    async def ask_1(state: State) -> State:
        called.append("ask_1")
        answer = interrupt("Question 1?")
        return {"value": [f"ask_1:{answer}"]}

    async def ask_2(state: State) -> State:
        called.append("ask_2")
        answer = interrupt("Question 2?")
        return {"value": [f"ask_2:{answer}"]}

    executor = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_1", ask_1)
        .add_node("ask_2", ask_2)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_1")
        .add_edge("ask_1", "ask_2")
        .add_edge("ask_2", "__end__")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(State)
        .add_node("executor", executor)
        .add_edge(START, "executor")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Run until first interrupt
    await graph.ainvoke({"value": []}, config)

    # Resume first interrupt
    result = await graph.ainvoke(Command(resume="answer_1"), config)
    assert result["__interrupt__"][0].value == "Question 2?"

    # Capture subgraph state at the second interrupt
    parent_state = await graph.aget_state(config, subgraphs=True)
    sub_config = parent_state.tasks[0].state.config

    # Resume second interrupt to complete
    await graph.ainvoke(Command(resume="answer_2"), config)

    # --- Scenario 1: Replay from subgraph checkpoint at 2nd interrupt ---
    called.clear()
    replay_result = await graph.ainvoke(None, sub_config)
    assert "__interrupt__" in replay_result
    assert replay_result["__interrupt__"][0].value == "Question 2?"
    assert "step_a" not in called
    assert "ask_1" not in called

    # --- Scenario 2: Fork from subgraph checkpoint at 2nd interrupt ---
    called.clear()
    fork_config = await graph.aupdate_state(sub_config, {"value": ["forked"]})
    fork_result = await graph.ainvoke(None, fork_config)
    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "Question 2?"
    assert "step_a" not in called
    assert "ask_1" not in called


@NEEDS_CONTEXTVARS
async def test_subgraph_time_travel_after_completion_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Time travel to a subgraph checkpoint AFTER both interrupts resolved (async)."""

    called: list[str] = []

    async def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["step_a_done"]}

    async def ask_1(state: State) -> State:
        called.append("ask_1")
        answer = interrupt("Question 1?")
        return {"value": [f"ask_1:{answer}"]}

    async def ask_2(state: State) -> State:
        called.append("ask_2")
        answer = interrupt("Question 2?")
        return {"value": [f"ask_2:{answer}"]}

    executor = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_1", ask_1)
        .add_node("ask_2", ask_2)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_1")
        .add_edge("ask_1", "ask_2")
        .add_edge("ask_2", "__end__")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(State)
        .add_node("executor", executor)
        .add_edge(START, "executor")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    await graph.ainvoke({"value": []}, config)
    await graph.ainvoke(Command(resume="answer_1"), config)
    await graph.ainvoke(Command(resume="answer_2"), config)

    final_state = await graph.aget_state(config)
    assert len(final_state.tasks) == 0

    # Replay from the final parent checkpoint — should be a no-op
    called.clear()
    replay_result = await graph.ainvoke(None, final_state.config)
    assert "__interrupt__" not in replay_result
    assert "step_a" not in called
    assert "ask_1" not in called
    assert "ask_2" not in called
    assert "step_a_done" in replay_result["value"]
    assert "ask_1:answer_1" in replay_result["value"]
    assert "ask_2:answer_2" in replay_result["value"]


@NEEDS_CONTEXTVARS
async def test_replay_from_before_interrupt_then_resume_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replay from checkpoint before interrupt node, then resume with a new
    answer and verify the graph completes with the new value.

    Graph: START --> node_a --> ask_human (interrupt) --> node_b --> END
    """

    called: list[str] = []

    async def node_a(state: State) -> State:
        called.append("node_a")
        return {"value": ["a"]}

    async def ask_human(state: State) -> State:
        called.append("ask_human")
        answer = interrupt("What is your input?")
        return {"value": [f"human:{answer}"]}

    async def node_b(state: State) -> State:
        called.append("node_b")
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("ask_human", ask_human)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "ask_human")
        .add_edge("ask_human", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # --- Original run: invoke until interrupt, then resume to complete ---
    await graph.ainvoke({"value": []}, config)
    await graph.ainvoke(Command(resume="old_answer"), config)

    original_history = [s async for s in graph.aget_state_history(config)]
    original = _checkpoint_summary(original_history)
    assert [(s["source"], s["next"], s["values"]) for s in original] == [
        ("loop", (), {"value": ["a", "human:old_answer", "b"]}),
        ("loop", ("node_b",), {"value": ["a", "human:old_answer"]}),
        ("loop", ("ask_human",), {"value": ["a"]}),
        ("loop", ("node_a",), {"value": []}),
        ("input", ("__start__",), {"value": []}),
    ]

    # --- Replay from checkpoint before ask_human ---
    before_ask = next(s for s in original_history if s.next == ("ask_human",))

    called.clear()
    replay_result = await graph.ainvoke(None, before_ask.config)
    assert replay_result["__interrupt__"][0].value == "What is your input?"
    assert "ask_human" in called
    assert "node_a" not in called

    # A fork checkpoint is now the latest
    post_replay = _checkpoint_summary(
        [s async for s in graph.aget_state_history(config)]
    )
    assert [(s["source"], s["next"]) for s in post_replay] == [
        ("fork", ("ask_human",)),
        ("loop", ()),
        ("loop", ("node_b",)),
        ("loop", ("ask_human",)),
        ("loop", ("node_a",)),
        ("input", ("__start__",)),
    ]

    # --- Resume with a new answer ---
    called.clear()
    final_result = await graph.ainvoke(Command(resume="new_answer"), config)
    assert final_result["value"] == ["a", "human:new_answer", "b"]
    assert "ask_human" in called
    assert "node_b" in called

    final = _checkpoint_summary([s async for s in graph.aget_state_history(config)])
    assert [(s["source"], s["next"], s["values"]) for s in final] == [
        # New branch (from fork)
        ("loop", (), {"value": ["a", "human:new_answer", "b"]}),
        ("loop", ("node_b",), {"value": ["a", "human:new_answer"]}),
        ("fork", ("ask_human",), {"value": ["a"]}),
        # Original branch (preserved)
        ("loop", (), {"value": ["a", "human:old_answer", "b"]}),
        ("loop", ("node_b",), {"value": ["a", "human:old_answer"]}),
        ("loop", ("ask_human",), {"value": ["a"]}),
        ("loop", ("node_a",), {"value": []}),
        ("input", ("__start__",), {"value": []}),
    ]


@NEEDS_CONTEXTVARS
async def test_subgraph_time_travel_resume_from_first_interrupt_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Time travel to a subgraph checkpoint at the first interrupt, then
    resume through both interrupts with new answers.

    Parent:    START --> executor (subgraph, checkpointer=True) --> END
    Executor:  START --> step_a --> ask_1 (interrupt) --> ask_2 (interrupt) --> END
    """

    called: list[str] = []

    async def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["step_a_done"]}

    async def ask_1(state: State) -> State:
        called.append("ask_1")
        answer = interrupt("Question 1?")
        return {"value": [f"ask_1:{answer}"]}

    async def ask_2(state: State) -> State:
        called.append("ask_2")
        answer = interrupt("Question 2?")
        return {"value": [f"ask_2:{answer}"]}

    executor = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_1", ask_1)
        .add_node("ask_2", ask_2)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_1")
        .add_edge("ask_1", "ask_2")
        .add_edge("ask_2", "__end__")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(State)
        .add_node("executor", executor)
        .add_edge(START, "executor")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # --- Original run: hit both interrupts and resume ---
    await graph.ainvoke({"value": []}, config)
    sub_config_at_first = (
        (await graph.aget_state(config, subgraphs=True)).tasks[0].state.config
    )
    await graph.ainvoke(Command(resume="answer_1"), config)
    await graph.ainvoke(Command(resume="answer_2"), config)

    original = _checkpoint_summary([s async for s in graph.aget_state_history(config)])
    assert [(s["source"], s["next"], s["values"]) for s in original] == [
        ("loop", (), {"value": ["step_a_done", "ask_1:answer_1", "ask_2:answer_2"]}),
        ("loop", ("executor",), {"value": []}),
        ("input", ("__start__",), {"value": []}),
    ]

    # --- Time travel to first interrupt's subgraph checkpoint ---
    called.clear()
    replay_result = await graph.ainvoke(None, sub_config_at_first)
    assert replay_result["__interrupt__"][0].value == "Question 1?"
    assert "step_a" not in called

    # Fork is now the latest parent checkpoint
    post_tt = _checkpoint_summary([s async for s in graph.aget_state_history(config)])
    assert [(s["source"], s["next"]) for s in post_tt] == [
        ("fork", ("executor",)),  # <-- new fork (latest)
        ("loop", ()),  # original done
        ("loop", ("executor",)),
        ("input", ("__start__",)),
    ]

    # --- Resume both interrupts with new answers ---
    called.clear()
    resume_1 = await graph.ainvoke(Command(resume="new_answer_1"), config)
    assert resume_1["__interrupt__"][0].value == "Question 2?"
    assert "ask_1" in called

    called.clear()
    resume_2 = await graph.ainvoke(Command(resume="new_answer_2"), config)
    assert resume_2["value"] == [
        "step_a_done",
        "ask_1:new_answer_1",
        "ask_2:new_answer_2",
    ]

    # Verify final history: original branch preserved, new branch appended
    final = _checkpoint_summary([s async for s in graph.aget_state_history(config)])
    assert [(s["source"], s["next"], s["values"]) for s in final] == [
        # New branch (from time travel fork)
        (
            "loop",
            (),
            {"value": ["step_a_done", "ask_1:new_answer_1", "ask_2:new_answer_2"]},
        ),
        ("fork", ("executor",), {"value": []}),
        # Original branch (preserved)
        ("loop", (), {"value": ["step_a_done", "ask_1:answer_1", "ask_2:answer_2"]}),
        ("loop", ("executor",), {"value": []}),
        ("input", ("__start__",), {"value": []}),
    ]


@NEEDS_CONTEXTVARS
async def test_subgraph_time_travel_resume_from_second_interrupt_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Time travel to a subgraph checkpoint at the second interrupt, then
    resume with a new answer. The first interrupt's answer should be preserved.

    Parent:    START --> executor (subgraph, checkpointer=True) --> END
    Executor:  START --> step_a --> ask_1 (interrupt) --> ask_2 (interrupt) --> END
    """

    called: list[str] = []

    async def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["step_a_done"]}

    async def ask_1(state: State) -> State:
        called.append("ask_1")
        answer = interrupt("Question 1?")
        return {"value": [f"ask_1:{answer}"]}

    async def ask_2(state: State) -> State:
        called.append("ask_2")
        answer = interrupt("Question 2?")
        return {"value": [f"ask_2:{answer}"]}

    executor = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_1", ask_1)
        .add_node("ask_2", ask_2)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_1")
        .add_edge("ask_1", "ask_2")
        .add_edge("ask_2", "__end__")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(State)
        .add_node("executor", executor)
        .add_edge(START, "executor")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # --- Original run: hit both interrupts and resume ---
    await graph.ainvoke({"value": []}, config)
    await graph.ainvoke(Command(resume="answer_1"), config)
    sub_config_at_second = (
        (await graph.aget_state(config, subgraphs=True)).tasks[0].state.config
    )
    await graph.ainvoke(Command(resume="answer_2"), config)

    original = _checkpoint_summary([s async for s in graph.aget_state_history(config)])
    assert [(s["source"], s["next"], s["values"]) for s in original] == [
        ("loop", (), {"value": ["step_a_done", "ask_1:answer_1", "ask_2:answer_2"]}),
        ("loop", ("executor",), {"value": []}),
        ("input", ("__start__",), {"value": []}),
    ]

    # --- Time travel to second interrupt ---
    called.clear()
    replay_result = await graph.ainvoke(None, sub_config_at_second)
    assert replay_result["__interrupt__"][0].value == "Question 2?"
    assert "step_a" not in called
    assert "ask_1" not in called

    # Fork is now the latest parent checkpoint
    post_tt = _checkpoint_summary([s async for s in graph.aget_state_history(config)])
    assert [(s["source"], s["next"]) for s in post_tt] == [
        ("fork", ("executor",)),  # <-- new fork (latest)
        ("loop", ()),  # original done
        ("loop", ("executor",)),
        ("input", ("__start__",)),
    ]

    # --- Resume with a new answer for ask_2 only ---
    called.clear()
    resume_result = await graph.ainvoke(Command(resume="new_answer_2"), config)
    assert resume_result["value"] == [
        "step_a_done",
        "ask_1:answer_1",
        "ask_2:new_answer_2",
    ]

    # Verify final history
    final = _checkpoint_summary([s async for s in graph.aget_state_history(config)])
    assert [(s["source"], s["next"], s["values"]) for s in final] == [
        # New branch (from time travel fork)
        (
            "loop",
            (),
            {"value": ["step_a_done", "ask_1:answer_1", "ask_2:new_answer_2"]},
        ),
        ("fork", ("executor",), {"value": []}),
        # Original branch (preserved)
        ("loop", (), {"value": ["step_a_done", "ask_1:answer_1", "ask_2:answer_2"]}),
        ("loop", ("executor",), {"value": []}),
        ("input", ("__start__",), {"value": []}),
    ]


@NEEDS_CONTEXTVARS
async def test_subgraph_time_travel_checkpoint_pattern_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Verify the checkpoint pattern created by time travel to a subgraph
    interrupt. A fork checkpoint should branch from the replay point.

    Parent:    START --> executor (subgraph, checkpointer=True) --> END
    Executor:  START --> ask (interrupt) --> END
    """

    async def ask(state: State) -> State:
        answer = interrupt("Q?")
        return {"value": [f"a:{answer}"]}

    executor = (
        StateGraph(State)
        .add_node("ask", ask)
        .add_edge(START, "ask")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(State)
        .add_node("executor", executor)
        .add_edge(START, "executor")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Run until interrupt, then complete
    await graph.ainvoke({"value": []}, config)
    sub_config = (await graph.aget_state(config, subgraphs=True)).tasks[0].state.config
    await graph.ainvoke(Command(resume="first"), config)

    original = _checkpoint_summary([s async for s in graph.aget_state_history(config)])
    assert [(s["source"], s["next"], s["values"]) for s in original] == [
        ("loop", (), {"value": ["a:first"]}),
        ("loop", ("executor",), {"value": []}),
        ("input", ("__start__",), {"value": []}),
    ]

    # Time travel to the interrupt
    await graph.ainvoke(None, sub_config)

    # Fork is now the latest, branching from the original replay point
    post_tt = [s async for s in graph.aget_state_history(config)]
    post_tt_summary = _checkpoint_summary(post_tt)
    assert [(s["source"], s["next"]) for s in post_tt_summary] == [
        ("fork", ("executor",)),  # <-- new fork (latest)
        ("loop", ()),
        ("loop", ("executor",)),  # <-- replay point / fork parent
        ("input", ("__start__",)),
    ]
    # Verify the fork's parent is the original replay point
    replay_point_id = sub_config["configurable"]["checkpoint_map"][""]
    assert post_tt[0].parent_config["configurable"]["checkpoint_id"] == replay_point_id

    # Resume from the fork
    result = await graph.ainvoke(Command(resume="second"), config)
    assert result["value"] == ["a:second"]

    final = _checkpoint_summary([s async for s in graph.aget_state_history(config)])
    assert [(s["source"], s["next"], s["values"]) for s in final] == [
        # New branch
        ("loop", (), {"value": ["a:second"]}),
        ("fork", ("executor",), {"value": []}),
        # Original branch
        ("loop", (), {"value": ["a:first"]}),
        ("loop", ("executor",), {"value": []}),
        ("input", ("__start__",), {"value": []}),
    ]


@NEEDS_CONTEXTVARS
async def test_3_levels_deep_time_travel_to_first_interrupt_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Time travel to innermost subgraph checkpoint at FIRST interrupt (async, 3 levels)."""

    called: list[str] = []

    async def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["step_a_done"]}

    async def ask_1(state: State) -> State:
        called.append("ask_1")
        answer = interrupt("Question 1?")
        return {"value": [f"ask_1:{answer}"]}

    async def ask_2(state: State) -> State:
        called.append("ask_2")
        answer = interrupt("Question 2?")
        return {"value": [f"ask_2:{answer}"]}

    inner = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_1", ask_1)
        .add_node("ask_2", ask_2)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_1")
        .add_edge("ask_1", "ask_2")
        .add_edge("ask_2", "__end__")
        .compile(checkpointer=True)
    )

    middle = (
        StateGraph(State)
        .add_node("inner", inner)
        .add_edge(START, "inner")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(State)
        .add_node("outer", middle)
        .add_edge(START, "outer")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    result = await graph.ainvoke({"value": []}, config)
    assert result["__interrupt__"][0].value == "Question 1?"

    parent_state = await graph.aget_state(config, subgraphs=True)
    mid_state = parent_state.tasks[0].state
    inner_config = mid_state.tasks[0].state.config

    await graph.ainvoke(Command(resume="answer_1"), config)
    await graph.ainvoke(Command(resume="answer_2"), config)

    # --- Scenario 1: Replay from innermost checkpoint at 1st interrupt ---
    called.clear()
    replay_result = await graph.ainvoke(None, inner_config)
    assert "__interrupt__" in replay_result
    assert replay_result["__interrupt__"][0].value == "Question 1?"
    assert "step_a" not in called
    assert "ask_1" in called

    # --- Scenario 2: Fork from innermost checkpoint at 1st interrupt ---
    called.clear()
    fork_config = await graph.aupdate_state(inner_config, {"value": ["forked"]})
    fork_result = await graph.ainvoke(None, fork_config)
    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "Question 1?"
    assert "step_a" not in called
    assert "ask_1" in called


@NEEDS_CONTEXTVARS
async def test_3_levels_deep_time_travel_to_second_interrupt_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Time travel to innermost subgraph checkpoint at SECOND interrupt (async, 3 levels)."""

    called: list[str] = []

    async def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["step_a_done"]}

    async def ask_1(state: State) -> State:
        called.append("ask_1")
        answer = interrupt("Question 1?")
        return {"value": [f"ask_1:{answer}"]}

    async def ask_2(state: State) -> State:
        called.append("ask_2")
        answer = interrupt("Question 2?")
        return {"value": [f"ask_2:{answer}"]}

    inner = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_1", ask_1)
        .add_node("ask_2", ask_2)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_1")
        .add_edge("ask_1", "ask_2")
        .add_edge("ask_2", "__end__")
        .compile(checkpointer=True)
    )

    middle = (
        StateGraph(State)
        .add_node("inner", inner)
        .add_edge(START, "inner")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(State)
        .add_node("outer", middle)
        .add_edge(START, "outer")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    await graph.ainvoke({"value": []}, config)

    result = await graph.ainvoke(Command(resume="answer_1"), config)
    assert result["__interrupt__"][0].value == "Question 2?"

    parent_state = await graph.aget_state(config, subgraphs=True)
    mid_state = parent_state.tasks[0].state
    inner_config = mid_state.tasks[0].state.config

    await graph.ainvoke(Command(resume="answer_2"), config)

    # --- Scenario 1: Replay ---
    called.clear()
    replay_result = await graph.ainvoke(None, inner_config)
    assert "__interrupt__" in replay_result
    assert replay_result["__interrupt__"][0].value == "Question 2?"
    assert "step_a" not in called
    assert "ask_1" not in called

    # --- Scenario 2: Fork ---
    called.clear()
    fork_config = await graph.aupdate_state(inner_config, {"value": ["forked"]})
    fork_result = await graph.ainvoke(None, fork_config)
    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "Question 2?"
    assert "step_a" not in called
    assert "ask_1" not in called


@NEEDS_CONTEXTVARS
async def test_3_levels_deep_time_travel_to_middle_subgraph_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Time travel to the MIDDLE-level subgraph checkpoint (async, 3 levels)."""

    called: list[str] = []

    async def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["step_a_done"]}

    async def ask_1(state: State) -> State:
        called.append("ask_1")
        answer = interrupt("Question 1?")
        return {"value": [f"ask_1:{answer}"]}

    async def ask_2(state: State) -> State:
        called.append("ask_2")
        answer = interrupt("Question 2?")
        return {"value": [f"ask_2:{answer}"]}

    inner = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_1", ask_1)
        .add_node("ask_2", ask_2)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_1")
        .add_edge("ask_1", "ask_2")
        .add_edge("ask_2", "__end__")
        .compile(checkpointer=True)
    )

    middle = (
        StateGraph(State)
        .add_node("inner", inner)
        .add_edge(START, "inner")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(State)
        .add_node("outer", middle)
        .add_edge(START, "outer")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    await graph.ainvoke({"value": []}, config)
    await graph.ainvoke(Command(resume="answer_1"), config)

    parent_state = await graph.aget_state(config, subgraphs=True)
    mid_config = parent_state.tasks[0].state.config

    await graph.ainvoke(Command(resume="answer_2"), config)

    # --- Scenario 1: Replay from middle-level subgraph checkpoint ---
    # The middle subgraph's checkpoint knows about the inner subgraph's state
    # via checkpoint_map, so the inner replays from the correct point.
    called.clear()
    replay_result = await graph.ainvoke(None, mid_config)
    assert "__interrupt__" in replay_result

    # --- Scenario 2: Fork from middle-level subgraph checkpoint ---
    called.clear()
    fork_config = await graph.aupdate_state(mid_config, {"value": ["forked"]})
    fork_result = await graph.ainvoke(None, fork_config)
    assert "__interrupt__" in fork_result


@NEEDS_CONTEXTVARS
async def test_3_levels_deep_middle_has_interrupts_async(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Time travel when the MIDDLE subgraph itself has interrupts (async)."""

    called: list[str] = []

    async def pre(state: State) -> State:
        called.append("pre")
        answer = interrupt("Pre-question?")
        return {"value": [f"pre:{answer}"]}

    async def step_a(state: State) -> State:
        called.append("step_a")
        return {"value": ["step_a_done"]}

    async def ask_1(state: State) -> State:
        called.append("ask_1")
        answer = interrupt("Question 1?")
        return {"value": [f"ask_1:{answer}"]}

    inner = (
        StateGraph(State)
        .add_node("step_a", step_a)
        .add_node("ask_1", ask_1)
        .add_edge(START, "step_a")
        .add_edge("step_a", "ask_1")
        .add_edge("ask_1", "__end__")
        .compile(checkpointer=True)
    )

    middle = (
        StateGraph(State)
        .add_node("pre", pre)
        .add_node("inner", inner)
        .add_edge(START, "pre")
        .add_edge("pre", "inner")
        .add_edge("inner", "__end__")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(State)
        .add_node("outer", middle)
        .add_edge(START, "outer")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Run until first interrupt (pre in middle subgraph)
    result = await graph.ainvoke({"value": []}, config)
    assert result["__interrupt__"][0].value == "Pre-question?"

    # Capture middle subgraph config at the pre interrupt
    parent_state = await graph.aget_state(config, subgraphs=True)
    mid_config_at_pre = parent_state.tasks[0].state.config

    # Resume pre, hits ask_1 in inner subgraph
    result = await graph.ainvoke(Command(resume="pre_answer"), config)
    assert result["__interrupt__"][0].value == "Question 1?"

    # Capture middle subgraph config at the ask_1 interrupt
    parent_state = await graph.aget_state(config, subgraphs=True)
    mid_config_at_ask1 = parent_state.tasks[0].state.config

    # Resume ask_1 to complete
    result = await graph.ainvoke(Command(resume="answer_1"), config)
    assert "__interrupt__" not in result

    # --- Time travel to middle checkpoint at pre interrupt ---
    called.clear()
    replay_result = await graph.ainvoke(None, mid_config_at_pre)
    assert "__interrupt__" in replay_result
    assert replay_result["__interrupt__"][0].value == "Pre-question?"
    assert "pre" in called
    assert "step_a" not in called
    assert "ask_1" not in called

    # Fork from middle checkpoint at pre interrupt
    called.clear()
    fork_config = await graph.aupdate_state(mid_config_at_pre, {"value": ["forked"]})
    fork_result = await graph.ainvoke(None, fork_config)
    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "Pre-question?"
    assert "pre" in called
    assert "step_a" not in called

    # --- Time travel to middle checkpoint at ask_1 interrupt ---
    called.clear()
    replay_result = await graph.ainvoke(None, mid_config_at_ask1)
    assert "__interrupt__" in replay_result
    assert replay_result["__interrupt__"][0].value == "Question 1?"
    assert "pre" not in called
    assert "ask_1" in called

    # Fork from middle checkpoint at ask_1 interrupt
    called.clear()
    fork_config = await graph.aupdate_state(mid_config_at_ask1, {"value": ["forked"]})
    fork_result = await graph.ainvoke(None, fork_config)
    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "Question 1?"
    assert "pre" not in called
    assert "ask_1" in called


# ---------------------------------------------------------------------------
# Section 6: __copy__ / update_state(None)
# ---------------------------------------------------------------------------


@NEEDS_CONTEXTVARS
async def test_copy_fork_retriggers_interrupt(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Fork using __copy__ (no state changes) from checkpoint before interrupt.
    The interrupt is re-triggered because __copy__ creates a new checkpoint
    without cached resume values. Resume with new answer to verify."""

    called: list[str] = []

    def node_a(state: State) -> State:
        called.append("node_a")
        return {"value": ["a"]}

    def ask_human(state: State) -> State:
        called.append("ask_human")
        answer = interrupt("What is your input?")
        return {"value": [f"human:{answer}"]}

    def node_b(state: State) -> State:
        called.append("node_b")
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("ask_human", ask_human)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "ask_human")
        .add_edge("ask_human", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    await graph.ainvoke({"value": []}, config)
    await graph.ainvoke(Command(resume="hello"), config)

    history = [s async for s in graph.aget_state_history(config)]
    before_ask = [s for s in history if s.next == ("ask_human",)][-1]

    called.clear()
    fork_config = await graph.aupdate_state(before_ask.config, None, as_node="__copy__")

    fork_result = await graph.ainvoke(None, fork_config)
    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "What is your input?"

    final = await graph.ainvoke(Command(resume="new_answer"), fork_config)
    assert final == {"value": ["a", "human:new_answer", "b"]}


@NEEDS_CONTEXTVARS
async def test_copy_fork_creates_sibling_checkpoint(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """__copy__ creates a checkpoint with source="fork", while regular
    update_state creates one with source="update"."""

    def node_a(state: State) -> State:
        return {"value": ["a"]}

    def node_b(state: State) -> State:
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    await graph.ainvoke({"value": []}, config)

    history = [s async for s in graph.aget_state_history(config)]
    before_b = next(s for s in history if s.next == ("node_b",))

    # __copy__ fork → source="fork"
    copy_config = await graph.aupdate_state(before_b.config, None, as_node="__copy__")
    copy_state = await graph.aget_state(copy_config)
    assert copy_state.metadata["source"] == "fork"

    # Regular update → source="update"
    regular_config = await graph.aupdate_state(before_b.config, {"value": ["x"]})
    regular_state = await graph.aget_state(regular_config)
    assert regular_state.metadata["source"] == "update"


@NEEDS_CONTEXTVARS
async def test_update_state_with_none_values(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """update_state with None values (not __copy__) goes through the normal
    update path, creating a new checkpoint that re-triggers interrupts."""

    def node_a(state: State) -> State:
        return {"value": ["a"]}

    def ask_human(state: State) -> State:
        answer = interrupt("What is your input?")
        return {"value": [f"human:{answer}"]}

    def node_b(state: State) -> State:
        return {"value": ["b"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("ask_human", ask_human)
        .add_node("node_b", node_b)
        .add_edge(START, "node_a")
        .add_edge("node_a", "ask_human")
        .add_edge("ask_human", "node_b")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    await graph.ainvoke({"value": []}, config)
    await graph.ainvoke(Command(resume="hello"), config)

    history = [s async for s in graph.aget_state_history(config)]
    before_ask = [s for s in history if s.next == ("ask_human",)][-1]

    fork_config = await graph.aupdate_state(before_ask.config, None)
    fork_result = await graph.ainvoke(None, fork_config)
    assert "__interrupt__" in fork_result
    assert fork_result["__interrupt__"][0].value == "What is your input?"

    fork_state = await graph.aget_state(fork_config)
    assert fork_state.metadata["source"] == "update"


# ---------------------------------------------------------------------------
# Section 7: Observability (get_state, config access)
# ---------------------------------------------------------------------------


@NEEDS_CONTEXTVARS
async def test_get_state_with_subgraphs_returns_subgraph_state(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """get_state(config, subgraphs=True) returns subgraph state and checkpoint
    config when paused at interrupt."""

    class SubState(TypedDict):
        data: str

    def sub_node(state: SubState) -> SubState:
        interrupt("Continue?")
        return {"data": "processed"}

    subgraph = (
        StateGraph(SubState)
        .add_node("process", sub_node)
        .add_edge(START, "process")
        .compile()
    )

    class ParentState(TypedDict):
        data: str

    graph = (
        StateGraph(ParentState)
        .add_node("sub", subgraph)
        .add_edge(START, "sub")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    await graph.ainvoke({"data": "input"}, config)

    state = await graph.aget_state(config, subgraphs=True)

    assert len(state.tasks) > 0
    sub_task = state.tasks[0]
    assert sub_task.state is not None

    sub_config = sub_task.state.config
    assert "configurable" in sub_config
    assert "thread_id" in sub_config["configurable"]


@NEEDS_CONTEXTVARS
async def test_checkpoint_ns_accessible_in_subgraph(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """RunnableConfig exposes checkpoint_ns and thread_id inside subgraph
    nodes."""

    captured_config: dict = {}

    class SubState(TypedDict):
        data: str

    def sub_node(state: SubState, config: RunnableConfig) -> SubState:
        captured_config["checkpoint_ns"] = config["configurable"].get("checkpoint_ns")
        captured_config["thread_id"] = config["configurable"].get("thread_id")
        return {"data": "done"}

    subgraph = (
        StateGraph(SubState)
        .add_node("inner", sub_node)
        .add_edge(START, "inner")
        .compile()
    )

    class ParentState(TypedDict):
        data: str

    graph = (
        StateGraph(ParentState)
        .add_node("outer", subgraph)
        .add_edge(START, "outer")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    await graph.ainvoke({"data": "test"}, config)

    assert captured_config["checkpoint_ns"] is not None
    assert captured_config["checkpoint_ns"] != ""
    assert captured_config["thread_id"] == "1"


# ---------------------------------------------------------------------------
# Section 8: Stateful vs stateless subgraph state retention on replay
# ---------------------------------------------------------------------------


@pytest.mark.skipif(
    sys.version_info < (3, 11),
    reason="Python 3.11+ required for async test",
)
async def test_stateful_subgraph_retains_state_on_parent_replay(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Stateful subgraph (checkpointer=True) remembers accumulated state
    from prior invocations when the parent replays."""
    started: list[tuple[str, dict]] = []
    observed: list[tuple[str, dict]] = []

    class SubState(TypedDict):
        value: Annotated[list[str], operator.add]

    class ParentState(TypedDict):
        results: Annotated[list[str], operator.add]

    def parent_node(state: ParentState) -> ParentState:
        return {"results": ["p"]}

    def step_a(state: SubState) -> SubState:
        started.append(("step_a", dict(state)))
        answer = interrupt("question_a")
        observed.append(("step_a", dict(state)))
        return {"value": [f"a:{answer}"]}

    def step_b(state: SubState) -> SubState:
        started.append(("step_b", dict(state)))
        answer = interrupt("question_b")
        observed.append(("step_b", dict(state)))
        return {"value": [f"b:{answer}"]}

    sub = (
        StateGraph(SubState)
        .add_node("step_a", step_a)
        .add_node("step_b", step_b)
        .add_edge(START, "step_a")
        .add_edge("step_a", "step_b")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(ParentState)
        .add_node("parent_node", parent_node)
        .add_node("sub_node", sub)
        .add_edge(START, "parent_node")
        .add_edge("parent_node", "sub_node")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # === 1st invocation: answer "a1" and "b1" ===
    await graph.ainvoke({"results": []}, config)  # hits step_a interrupt
    await graph.ainvoke(Command(resume="a1"), config)  # hits step_b interrupt
    await graph.ainvoke(Command(resume="b1"), config)  # completes

    # step_a saw empty state (fresh subgraph)
    assert observed[0] == ("step_a", {"value": []})
    # step_b saw step_a's answer
    assert observed[1] == ("step_b", {"value": ["a:a1"]})

    # === 2nd invocation: answer "a2" and "b2" ===
    observed.clear()
    await graph.ainvoke({"results": []}, config)  # hits step_a interrupt
    await graph.ainvoke(Command(resume="a2"), config)  # hits step_b interrupt
    await graph.ainvoke(Command(resume="b2"), config)  # completes

    # Stateful subgraph retained state from 1st invocation
    assert observed[0] == ("step_a", {"value": ["a:a1", "b:b1"]})
    assert observed[1] == ("step_b", {"value": ["a:a1", "b:b1", "a:a2"]})

    # === Replay from checkpoint before sub_node in 2nd invocation ===
    history = [s async for s in graph.aget_state_history(config)]
    # History is newest-first, so first match = 2nd invocation
    before_sub_2nd = [s for s in history if s.next == ("sub_node",)][0]

    started.clear()
    replay = await graph.ainvoke(None, before_sub_2nd.config)

    assert "__interrupt__" in replay
    # Replay sees 1st invocation's final state, NOT 2nd invocation's
    assert started[0] == ("step_a", {"value": ["a:a1", "b:b1"]})


@pytest.mark.skipif(
    sys.version_info < (3, 11),
    reason="Python 3.11+ required for async test",
)
async def test_stateful_subgraph_retains_state_on_parent_fork(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Stateful subgraph (checkpointer=True) remembers accumulated state
    from prior invocations when the parent forks."""
    observed: list[tuple[str, dict]] = []

    class SubState(TypedDict):
        value: Annotated[list[str], operator.add]

    class ParentState(TypedDict):
        results: Annotated[list[str], operator.add]

    def parent_node(state: ParentState) -> ParentState:
        return {"results": ["p"]}

    def step_a(state: SubState) -> SubState:
        observed.append(("step_a", dict(state)))
        answer = interrupt("question_a")
        return {"value": [f"a:{answer}"]}

    def step_b(state: SubState) -> SubState:
        observed.append(("step_b", dict(state)))
        answer = interrupt("question_b")
        return {"value": [f"b:{answer}"]}

    sub = (
        StateGraph(SubState)
        .add_node("step_a", step_a)
        .add_node("step_b", step_b)
        .add_edge(START, "step_a")
        .add_edge("step_a", "step_b")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(ParentState)
        .add_node("parent_node", parent_node)
        .add_node("sub_node", sub)
        .add_edge(START, "parent_node")
        .add_edge("parent_node", "sub_node")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # === 1st invocation: answer "a1" and "b1" ===
    await graph.ainvoke({"results": []}, config)  # hits step_a interrupt
    await graph.ainvoke(Command(resume="a1"), config)  # hits step_b interrupt
    await graph.ainvoke(Command(resume="b1"), config)  # completes

    # === 2nd invocation: answer "a2" and "b2" ===
    await graph.ainvoke({"results": []}, config)  # hits step_a interrupt
    await graph.ainvoke(Command(resume="a2"), config)  # hits step_b interrupt
    await graph.ainvoke(Command(resume="b2"), config)  # completes

    # === Fork from checkpoint before sub_node in 2nd invocation ===
    history = [s async for s in graph.aget_state_history(config)]
    before_sub_2nd = [s for s in history if s.next == ("sub_node",)][0]

    fork_config = await graph.aupdate_state(
        before_sub_2nd.config, {"results": ["forked"]}
    )

    observed.clear()
    fork_result = await graph.ainvoke(None, fork_config)

    assert "__interrupt__" in fork_result
    # Fork sees 1st invocation's final state, NOT 2nd invocation's
    assert observed[0] == ("step_a", {"value": ["a:a1", "b:b1"]})


@pytest.mark.skipif(
    sys.version_info < (3, 11),
    reason="Python 3.11+ required for async test",
)
# ---------------------------------------------------------------------------
# Section 8: Append-only checkpoint history (branching / forking)
# ---------------------------------------------------------------------------


@NEEDS_CONTEXTVARS
async def test_replay_creates_branch_preserving_old_checkpoints(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replaying from a mid-run checkpoint creates a new branch of checkpoints
    while the original checkpoint sequence is preserved (append-only).

    Original run (newest first):
      C4  next=()          values=[a, b1, c]     parent=C3
      C3  next=(node_c,)   values=[a, b1]        parent=C2
      C2  next=(node_b,)   values=[a]            parent=C1
      C1  next=(node_a,)   values=[]             parent=C0
      C0  next=(__start__,) values={}             parent=None

    After replay from C2 (newest first):
      C6  next=()          values=[a, b2, c]     parent=C5   <- new branch tip
      C5  next=(node_c,)   values=[a, b2]        parent=C2   <- branches from C2
      C4  next=()          values=[a, b1, c]     parent=C3   <- old branch preserved
      C3  next=(node_c,)   values=[a, b1]        parent=C2
      C2  next=(node_b,)   values=[a]            parent=C1
      C1  next=(node_a,)   values=[]             parent=C0
      C0  next=(__start__,) values={}             parent=None
    """

    call_count = 0

    def node_a(state: State) -> State:
        return {"value": ["a"]}

    def node_b(state: State) -> State:
        nonlocal call_count
        call_count += 1
        return {"value": [f"b{call_count}"]}

    def node_c(state: State) -> State:
        return {"value": ["c"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("node_b", node_b)
        .add_node("node_c", node_c)
        .add_edge(START, "node_a")
        .add_edge("node_a", "node_b")
        .add_edge("node_b", "node_c")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    result = await graph.ainvoke({"value": []}, config)
    assert result == {"value": ["a", "b1", "c"]}

    # -- Original checkpoint history (newest first) --
    original_history = [s async for s in graph.aget_state_history(config)]
    original_summary = _checkpoint_summary(original_history)
    assert len(original_summary) == 5
    assert [s["next"] for s in original_summary] == [
        (),
        ("node_c",),
        ("node_b",),
        ("node_a",),
        ("__start__",),
    ]
    assert [s["values"] for s in original_summary] == [
        {"value": ["a", "b1", "c"]},
        {"value": ["a", "b1"]},
        {"value": ["a"]},
        {"value": []},
        {"value": []},
    ]
    original_ids = {s.config["configurable"]["checkpoint_id"] for s in original_history}

    # Find checkpoint before node_b and replay from it
    before_b = next(s for s in original_history if s.next == ("node_b",))
    before_b_id = before_b.config["configurable"]["checkpoint_id"]
    replay_result = await graph.ainvoke(None, before_b.config)
    assert replay_result == {"value": ["a", "b2", "c"]}

    # -- Post-replay checkpoint history (newest first) --
    post_replay_history = [s async for s in graph.aget_state_history(config)]
    post_summary = _checkpoint_summary(post_replay_history)
    # 5 original + 1 fork + 2 new branch checkpoints = 8
    assert len(post_summary) == 8

    assert [s["next"] for s in post_summary] == [
        (),  # new branch tip
        ("node_c",),  # new branch
        ("node_b",),  # fork from replay point
        (),  # old branch tip
        ("node_c",),  # old
        ("node_b",),  # branch point (C2)
        ("node_a",),  # old (C1)
        ("__start__",),  # old (C0)
    ]
    assert [s["values"] for s in post_summary] == [
        {"value": ["a", "b2", "c"]},  # new branch tip
        {"value": ["a", "b2"]},  # new: node_b re-ran with call_count=2
        {"value": ["a"]},  # fork from replay point
        {"value": ["a", "b1", "c"]},  # old branch tip preserved
        {"value": ["a", "b1"]},  # old
        {"value": ["a"]},  # branch point
        {"value": []},  # old
        {"value": []},  # old
    ]

    # All original checkpoint IDs still exist (append-only)
    post_ids = {s.config["configurable"]["checkpoint_id"] for s in post_replay_history}
    assert original_ids.issubset(post_ids)

    # New branch's oldest checkpoint parent is the branch point
    new_checkpoints = [
        s
        for s in post_replay_history
        if s.config["configurable"]["checkpoint_id"] not in original_ids
    ]
    oldest_new = sorted(new_checkpoints, key=lambda s: s.created_at)[0]
    assert oldest_new.parent_config is not None
    assert oldest_new.parent_config["configurable"]["checkpoint_id"] == before_b_id

    # get_state returns the new branch tip
    latest = await graph.aget_state(config)
    assert latest.values == {"value": ["a", "b2", "c"]}
    assert latest.config["configurable"]["checkpoint_id"] not in original_ids


@NEEDS_CONTEXTVARS
async def test_replay_creates_branch_in_subgraph(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replaying a graph with a subgraph from a mid-run checkpoint creates a
    new branch while preserving the original checkpoint sequence.

    The subgraph re-executes on the new branch and the old checkpoints
    (including sub-checkpoints) remain in the history.
    """

    sub_call_count = 0

    class SubState(TypedDict):
        sub_value: Annotated[list[str], operator.add]

    class ParentState(TypedDict):
        value: Annotated[list[str], operator.add]
        sub_value: Annotated[list[str], operator.add]

    def parent_start(state: ParentState) -> ParentState:
        return {"value": ["p_start"]}

    def sub_step(state: SubState) -> SubState:
        nonlocal sub_call_count
        sub_call_count += 1
        return {"sub_value": [f"sub{sub_call_count}"]}

    def parent_end(state: ParentState) -> ParentState:
        return {"value": ["p_end"]}

    sub = (
        StateGraph(SubState)
        .add_node("sub_step", sub_step)
        .add_edge(START, "sub_step")
        .compile()
    )

    graph = (
        StateGraph(ParentState)
        .add_node("parent_start", parent_start)
        .add_node("sub_graph", sub)
        .add_node("parent_end", parent_end)
        .add_edge(START, "parent_start")
        .add_edge("parent_start", "sub_graph")
        .add_edge("sub_graph", "parent_end")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    result = await graph.ainvoke({"value": [], "sub_value": []}, config)
    assert result == {"value": ["p_start", "p_end"], "sub_value": ["sub1"]}

    # Capture original checkpoint IDs
    original_history = [s async for s in graph.aget_state_history(config)]
    original_ids = {s.config["configurable"]["checkpoint_id"] for s in original_history}

    # Find checkpoint before sub_graph
    before_sub = next(s for s in original_history if s.next == ("sub_graph",))
    before_sub_id = before_sub.config["configurable"]["checkpoint_id"]

    # Replay from before sub_graph
    replay_result = await graph.ainvoke(None, before_sub.config)
    assert replay_result == {"value": ["p_start", "p_end"], "sub_value": ["sub2"]}

    # Get full history after replay
    post_replay_history = [s async for s in graph.aget_state_history(config)]
    post_replay_ids = {
        s.config["configurable"]["checkpoint_id"] for s in post_replay_history
    }

    # All original checkpoint IDs still exist (append-only)
    assert original_ids.issubset(post_replay_ids)

    # New checkpoints were added (the branch)
    new_ids = post_replay_ids - original_ids
    assert len(new_ids) >= 2  # sub_graph + parent_end at minimum

    # The oldest new checkpoint's parent is the checkpoint we replayed from
    new_checkpoints = [
        s
        for s in post_replay_history
        if s.config["configurable"]["checkpoint_id"] in new_ids
    ]
    oldest_new = sorted(new_checkpoints, key=lambda s: s.created_at)[0]
    assert oldest_new.parent_config is not None
    assert oldest_new.parent_config["configurable"]["checkpoint_id"] == before_sub_id

    # get_state returns the new branch tip
    latest = await graph.aget_state(config)
    assert latest.config["configurable"]["checkpoint_id"] in new_ids
    assert latest.values == {"value": ["p_start", "p_end"], "sub_value": ["sub2"]}


@NEEDS_CONTEXTVARS
async def test_fork_creates_branch_preserving_old_checkpoints(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Forking (update_state + invoke) from a mid-run checkpoint creates a new
    branch of checkpoints while the original sequence is preserved.

    Original run (newest first):
      C4  next=()          values=[a, b1, c]     parent=C3
      C3  next=(node_c,)   values=[a, b1]        parent=C2
      C2  next=(node_b,)   values=[a]            parent=C1
      C1  next=(node_a,)   values=[]             parent=C0
      C0  next=(__start__,) values={}             parent=None

    After fork from C2 with update {"value": ["x"]} (newest first):
      C7  next=()          values=[a, x, b2, c]  parent=C6
      C6  next=(node_c,)   values=[a, x, b2]     parent=C5
      C5  next=(node_b,)   values=[a, x]         parent=C2   <- fork checkpoint
      C4  next=()          values=[a, b1, c]     parent=C3   <- old branch preserved
      C3  next=(node_c,)   values=[a, b1]        parent=C2
      C2  next=(node_b,)   values=[a]            parent=C1   <- fork point
      C1  next=(node_a,)   values=[]             parent=C0
      C0  next=(__start__,) values={}             parent=None
    """

    call_count = 0

    def node_a(state: State) -> State:
        return {"value": ["a"]}

    def node_b(state: State) -> State:
        nonlocal call_count
        call_count += 1
        return {"value": [f"b{call_count}"]}

    def node_c(state: State) -> State:
        return {"value": ["c"]}

    graph = (
        StateGraph(State)
        .add_node("node_a", node_a)
        .add_node("node_b", node_b)
        .add_node("node_c", node_c)
        .add_edge(START, "node_a")
        .add_edge("node_a", "node_b")
        .add_edge("node_b", "node_c")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    result = await graph.ainvoke({"value": []}, config)
    assert result == {"value": ["a", "b1", "c"]}

    # -- Original checkpoint history (newest first) --
    original_history = [s async for s in graph.aget_state_history(config)]
    original_summary = _checkpoint_summary(original_history)
    assert len(original_summary) == 5
    assert [s["next"] for s in original_summary] == [
        (),
        ("node_c",),
        ("node_b",),
        ("node_a",),
        ("__start__",),
    ]
    assert [s["values"] for s in original_summary] == [
        {"value": ["a", "b1", "c"]},
        {"value": ["a", "b1"]},
        {"value": ["a"]},
        {"value": []},
        {"value": []},
    ]
    original_ids = {s.config["configurable"]["checkpoint_id"] for s in original_history}

    # Fork from before node_b with modified state
    before_b = next(s for s in original_history if s.next == ("node_b",))
    before_b_id = before_b.config["configurable"]["checkpoint_id"]
    fork_config = await graph.aupdate_state(before_b.config, {"value": ["x"]})
    fork_result = await graph.ainvoke(None, fork_config)
    assert fork_result == {"value": ["a", "x", "b2", "c"]}

    # -- Post-fork checkpoint history (newest first) --
    post_fork_history = [s async for s in graph.aget_state_history(config)]
    post_summary = _checkpoint_summary(post_fork_history)
    # 5 original + 1 fork checkpoint (update_state) + 2 new nodes (node_b, node_c)
    assert len(post_summary) == 8

    assert [s["next"] for s in post_summary] == [
        (),  # new branch tip (C7)
        ("node_c",),  # new branch (C6)
        ("node_b",),  # fork checkpoint from update_state (C5)
        (),  # old branch tip (C4)
        ("node_c",),  # old (C3)
        ("node_b",),  # fork point (C2)
        ("node_a",),  # old (C1)
        ("__start__",),  # old (C0)
    ]
    assert [s["values"] for s in post_summary] == [
        {"value": ["a", "x", "b2", "c"]},  # new branch tip
        {"value": ["a", "x", "b2"]},  # new: node_b re-ran
        {"value": ["a", "x"]},  # fork: state updated with "x"
        {"value": ["a", "b1", "c"]},  # old branch tip preserved
        {"value": ["a", "b1"]},  # old
        {"value": ["a"]},  # fork point
        {"value": []},  # old
        {"value": []},  # old
    ]

    # All original checkpoint IDs still exist (append-only)
    post_ids = {s.config["configurable"]["checkpoint_id"] for s in post_fork_history}
    assert original_ids.issubset(post_ids)

    # Fork checkpoint's parent is the branch point
    new_checkpoints = [
        s
        for s in post_fork_history
        if s.config["configurable"]["checkpoint_id"] not in original_ids
    ]
    oldest_new = sorted(new_checkpoints, key=lambda s: s.created_at)[0]
    assert oldest_new.parent_config is not None
    assert oldest_new.parent_config["configurable"]["checkpoint_id"] == before_b_id

    # get_state returns the new branch tip
    latest = await graph.aget_state(config)
    assert latest.values == {"value": ["a", "x", "b2", "c"]}
    assert latest.config["configurable"]["checkpoint_id"] not in original_ids


@NEEDS_CONTEXTVARS
async def test_fork_creates_branch_in_subgraph(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Forking a graph with a subgraph from a mid-run checkpoint creates a new
    branch while preserving the original checkpoint sequence.

    The subgraph re-executes on the new branch and the old checkpoints remain.
    """

    sub_call_count = 0

    class SubState(TypedDict):
        sub_value: Annotated[list[str], operator.add]

    class ParentState(TypedDict):
        value: Annotated[list[str], operator.add]
        sub_value: Annotated[list[str], operator.add]

    def parent_start(state: ParentState) -> ParentState:
        return {"value": ["p_start"]}

    def sub_step(state: SubState) -> SubState:
        nonlocal sub_call_count
        sub_call_count += 1
        return {"sub_value": [f"sub{sub_call_count}"]}

    def parent_end(state: ParentState) -> ParentState:
        return {"value": ["p_end"]}

    sub = (
        StateGraph(SubState)
        .add_node("sub_step", sub_step)
        .add_edge(START, "sub_step")
        .compile()
    )

    graph = (
        StateGraph(ParentState)
        .add_node("parent_start", parent_start)
        .add_node("sub_graph", sub)
        .add_node("parent_end", parent_end)
        .add_edge(START, "parent_start")
        .add_edge("parent_start", "sub_graph")
        .add_edge("sub_graph", "parent_end")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}
    result = await graph.ainvoke({"value": [], "sub_value": []}, config)
    assert result == {"value": ["p_start", "p_end"], "sub_value": ["sub1"]}

    # Capture original checkpoint IDs
    original_history = [s async for s in graph.aget_state_history(config)]
    original_ids = {s.config["configurable"]["checkpoint_id"] for s in original_history}

    # Find checkpoint before sub_graph and fork with modified state
    before_sub = next(s for s in original_history if s.next == ("sub_graph",))
    before_sub_id = before_sub.config["configurable"]["checkpoint_id"]
    fork_config = await graph.aupdate_state(before_sub.config, {"value": ["extra"]})
    fork_result = await graph.ainvoke(None, fork_config)
    assert fork_result == {
        "value": ["p_start", "extra", "p_end"],
        "sub_value": ["sub2"],
    }

    # Get full history after fork
    post_fork_history = [s async for s in graph.aget_state_history(config)]
    post_fork_ids = {
        s.config["configurable"]["checkpoint_id"] for s in post_fork_history
    }

    # All original checkpoint IDs still exist (append-only)
    assert original_ids.issubset(post_fork_ids)

    # New checkpoints were added (the branch)
    new_ids = post_fork_ids - original_ids
    assert len(new_ids) >= 3  # fork checkpoint + sub_graph + parent_end

    # The oldest new checkpoint's parent is the checkpoint we forked from
    new_checkpoints = [
        s
        for s in post_fork_history
        if s.config["configurable"]["checkpoint_id"] in new_ids
    ]
    oldest_new = sorted(new_checkpoints, key=lambda s: s.created_at)[0]
    assert oldest_new.parent_config is not None
    assert oldest_new.parent_config["configurable"]["checkpoint_id"] == before_sub_id

    # get_state returns the new branch tip
    latest = await graph.aget_state(config)
    assert latest.config["configurable"]["checkpoint_id"] in new_ids
    assert latest.values == {
        "value": ["p_start", "extra", "p_end"],
        "sub_value": ["sub2"],
    }


@NEEDS_CONTEXTVARS
async def test_stateless_subgraph_starts_fresh_on_parent_replay(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Stateless subgraph (no checkpointer) always starts with empty state,
    even after prior invocations have completed."""
    started: list[tuple[str, dict]] = []
    observed: list[tuple[str, dict]] = []

    class SubState(TypedDict):
        value: Annotated[list[str], operator.add]

    class ParentState(TypedDict):
        results: Annotated[list[str], operator.add]

    def parent_node(state: ParentState) -> ParentState:
        return {"results": ["p"]}

    def step_a(state: SubState) -> SubState:
        started.append(("step_a", dict(state)))
        answer = interrupt("question_a")
        observed.append(("step_a", dict(state)))
        return {"value": [f"a:{answer}"]}

    def step_b(state: SubState) -> SubState:
        started.append(("step_b", dict(state)))
        answer = interrupt("question_b")
        observed.append(("step_b", dict(state)))
        return {"value": [f"b:{answer}"]}

    sub = (
        StateGraph(SubState)
        .add_node("step_a", step_a)
        .add_node("step_b", step_b)
        .add_edge(START, "step_a")
        .add_edge("step_a", "step_b")
        .compile()  # no checkpointer — stateless
    )

    graph = (
        StateGraph(ParentState)
        .add_node("parent_node", parent_node)
        .add_node("sub_node", sub)
        .add_edge(START, "parent_node")
        .add_edge("parent_node", "sub_node")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # === 1st invocation: answer "a1" and "b1" ===
    await graph.ainvoke({"results": []}, config)  # hits step_a interrupt
    await graph.ainvoke(Command(resume="a1"), config)  # hits step_b interrupt
    await graph.ainvoke(Command(resume="b1"), config)  # completes

    # step_a saw empty state, step_b saw only step_a's answer
    assert observed[0] == ("step_a", {"value": []})
    assert observed[1] == ("step_b", {"value": ["a:a1"]})

    # === 2nd invocation: answer "a2" and "b2" ===
    observed.clear()
    await graph.ainvoke({"results": []}, config)  # hits step_a interrupt
    await graph.ainvoke(Command(resume="a2"), config)  # hits step_b interrupt
    await graph.ainvoke(Command(resume="b2"), config)  # completes

    # Stateless subgraph starts fresh — no memory of 1st invocation
    assert observed[0] == ("step_a", {"value": []})
    assert observed[1] == ("step_b", {"value": ["a:a2"]})

    # === Replay from checkpoint before sub_node in 2nd invocation ===
    history = [s async for s in graph.aget_state_history(config)]
    before_sub_2nd = [s for s in history if s.next == ("sub_node",)][0]

    started.clear()
    replay = await graph.ainvoke(None, before_sub_2nd.config)

    assert "__interrupt__" in replay
    # Stateless subgraph starts completely fresh on replay
    assert started[0] == ("step_a", {"value": []})


@NEEDS_CONTEXTVARS
async def test_stateful_subgraph_loads_latest_after_replay(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """After replaying a parent checkpoint, a subsequent (3rd) invocation should
    load the subgraph state created by the replay — not the state from the
    checkpoint we replayed from."""
    observed: list[tuple[str, dict]] = []

    class SubState(TypedDict):
        value: Annotated[list[str], operator.add]

    class ParentState(TypedDict):
        results: Annotated[list[str], operator.add]

    def parent_node(state: ParentState) -> ParentState:
        return {"results": ["p"]}

    def sub_step(state: SubState) -> SubState:
        observed.append(("sub_step", dict(state)))
        return {"value": ["s"]}

    sub = (
        StateGraph(SubState)
        .add_node("sub_step", sub_step)
        .add_edge(START, "sub_step")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(ParentState)
        .add_node("parent_node", parent_node)
        .add_node("sub_node", sub)
        .add_edge(START, "parent_node")
        .add_edge("parent_node", "sub_node")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # 1st invocation — subgraph starts fresh
    await graph.ainvoke({"results": []}, config)
    assert observed[-1] == ("sub_step", {"value": []})

    # 2nd invocation — subgraph sees state from 1st
    await graph.ainvoke({"results": []}, config)
    assert observed[-1] == ("sub_step", {"value": ["s"]})

    # Replay from checkpoint before parent_node in 2nd invocation
    history = [s async for s in graph.aget_state_history(config)]
    before_parent_2nd = [s for s in history if s.next == ("parent_node",)][0]

    observed.clear()
    await graph.ainvoke(None, before_parent_2nd.config)
    # Replay should load subgraph state from end of 1st invocation
    assert observed[0] == ("sub_step", {"value": ["s"]})

    # 3rd invocation — should see state from the replay (2 × "s"), not from
    # the checkpoint we replayed from (1 × "s")
    observed.clear()
    await graph.ainvoke({"results": []}, config)
    assert observed[0] == ("sub_step", {"value": ["s", "s"]})


@NEEDS_CONTEXTVARS
async def test_three_level_nested_subgraph_loads_state_on_replay(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Three levels of nesting: parent -> mid -> inner.
    Replaying from the parent should load correct state at all levels."""
    observed: list[tuple[str, dict]] = []

    class InnerState(TypedDict):
        inner_trail: Annotated[list[str], operator.add]

    class MidState(TypedDict):
        mid_trail: Annotated[list[str], operator.add]

    class ParentState(TypedDict):
        results: Annotated[list[str], operator.add]

    def inner_step(state: InnerState) -> InnerState:
        observed.append(("inner_step", dict(state)))
        return {"inner_trail": ["inner"]}

    def mid_step(state: MidState) -> MidState:
        observed.append(("mid_step", dict(state)))
        return {"mid_trail": ["mid"]}

    def parent_step(state: ParentState) -> ParentState:
        return {"results": ["p"]}

    inner = (
        StateGraph(InnerState)
        .add_node("inner_step", inner_step)
        .add_edge(START, "inner_step")
        .compile(checkpointer=True)
    )

    mid = (
        StateGraph(MidState)
        .add_node("mid_step", mid_step)
        .add_node("inner_node", inner)
        .add_edge(START, "mid_step")
        .add_edge("mid_step", "inner_node")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(ParentState)
        .add_node("parent_step", parent_step)
        .add_node("mid_node", mid)
        .add_edge(START, "parent_step")
        .add_edge("parent_step", "mid_node")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # 1st invocation — everything starts fresh
    await graph.ainvoke({"results": []}, config)
    assert observed == [
        ("mid_step", {"mid_trail": []}),
        ("inner_step", {"inner_trail": []}),
    ]

    # 2nd invocation — both levels see accumulated state
    observed.clear()
    await graph.ainvoke({"results": []}, config)
    assert observed == [
        ("mid_step", {"mid_trail": ["mid"]}),
        ("inner_step", {"inner_trail": ["inner"]}),
    ]

    # Replay from checkpoint before parent_step in 2nd invocation
    history = [s async for s in graph.aget_state_history(config)]
    before_parent_2nd = [s for s in history if s.next == ("parent_step",)][0]

    observed.clear()
    await graph.ainvoke(None, before_parent_2nd.config)

    # Both mid and inner should load state from end of 1st invocation
    assert observed == [
        ("mid_step", {"mid_trail": ["mid"]}),
        ("inner_step", {"inner_trail": ["inner"]}),
    ]

    # 3rd invocation — sees state from replay, not from the replayed checkpoint
    observed.clear()
    await graph.ainvoke({"results": []}, config)
    assert observed == [
        ("mid_step", {"mid_trail": ["mid", "mid"]}),
        ("inner_step", {"inner_trail": ["inner", "inner"]}),
    ]


@NEEDS_CONTEXTVARS
async def test_three_level_nested_subgraph_loads_state_on_fork(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Three levels of nesting with fork instead of replay."""
    observed: list[tuple[str, dict]] = []

    class InnerState(TypedDict):
        inner_trail: Annotated[list[str], operator.add]

    class MidState(TypedDict):
        mid_trail: Annotated[list[str], operator.add]

    class ParentState(TypedDict):
        results: Annotated[list[str], operator.add]

    def inner_step(state: InnerState) -> InnerState:
        observed.append(("inner_step", dict(state)))
        return {"inner_trail": ["inner"]}

    def mid_step(state: MidState) -> MidState:
        observed.append(("mid_step", dict(state)))
        return {"mid_trail": ["mid"]}

    def parent_step(state: ParentState) -> ParentState:
        return {"results": ["p"]}

    inner = (
        StateGraph(InnerState)
        .add_node("inner_step", inner_step)
        .add_edge(START, "inner_step")
        .compile(checkpointer=True)
    )

    mid = (
        StateGraph(MidState)
        .add_node("mid_step", mid_step)
        .add_node("inner_node", inner)
        .add_edge(START, "mid_step")
        .add_edge("mid_step", "inner_node")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(ParentState)
        .add_node("parent_step", parent_step)
        .add_node("mid_node", mid)
        .add_edge(START, "parent_step")
        .add_edge("parent_step", "mid_node")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # 1st invocation
    await graph.ainvoke({"results": []}, config)

    # 2nd invocation
    await graph.ainvoke({"results": []}, config)

    # Fork from checkpoint before parent_step in 2nd invocation
    history = [s async for s in graph.aget_state_history(config)]
    before_parent_2nd = [s for s in history if s.next == ("parent_step",)][0]
    fork_config = await graph.aupdate_state(
        before_parent_2nd.config, {"results": ["forked"]}
    )

    observed.clear()
    await graph.ainvoke(None, fork_config)

    # Both mid and inner should load state from end of 1st invocation
    assert observed == [
        ("mid_step", {"mid_trail": ["mid"]}),
        ("inner_step", {"inner_trail": ["inner"]}),
    ]


@NEEDS_CONTEXTVARS
async def test_replay_from_first_invocation_checkpoint(
    async_checkpointer: BaseCheckpointSaver,
) -> None:
    """Replaying from the 1st invocation's checkpoint should load the subgraph
    state from before that invocation (i.e. empty)."""
    observed: list[tuple[str, dict]] = []

    class SubState(TypedDict):
        value: Annotated[list[str], operator.add]

    class ParentState(TypedDict):
        results: Annotated[list[str], operator.add]

    def parent_node(state: ParentState) -> ParentState:
        return {"results": ["p"]}

    def sub_step(state: SubState) -> SubState:
        observed.append(("sub_step", dict(state)))
        return {"value": ["s"]}

    sub = (
        StateGraph(SubState)
        .add_node("sub_step", sub_step)
        .add_edge(START, "sub_step")
        .compile(checkpointer=True)
    )

    graph = (
        StateGraph(ParentState)
        .add_node("parent_node", parent_node)
        .add_node("sub_node", sub)
        .add_edge(START, "parent_node")
        .add_edge("parent_node", "sub_node")
        .compile(checkpointer=async_checkpointer)
    )

    config = {"configurable": {"thread_id": "1"}}

    # Run twice so subgraph accumulates state
    await graph.ainvoke({"results": []}, config)
    await graph.ainvoke({"results": []}, config)

    # Replay from before sub_node in 1st invocation (furthest back)
    history = [s async for s in graph.aget_state_history(config)]
    before_sub_1st = [s for s in history if s.next == ("sub_node",)][-1]

    observed.clear()
    await graph.ainvoke(None, before_sub_1st.config)
    # Should see empty state — no prior subgraph checkpoints exist
    assert observed[0] == ("sub_step", {"value": []})
