/*
 * Copyright 2025 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package adk

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"sync"
	"sync/atomic"
	"testing"

	"github.com/stretchr/testify/assert"

	"github.com/cloudwego/eino/components/model"
	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/compose"
	"github.com/cloudwego/eino/schema"
)

type interruptTestToolsHandler struct {
	*BaseChatModelAgentMiddleware
	tools []tool.BaseTool
}

func TestPreprocessADKCheckpoint(t *testing.T) {
	t.Run("no-op when missing markers", func(t *testing.T) {
		in := []byte("random")
		out := preprocessADKCheckpoint(append([]byte(nil), in...))
		assert.Equal(t, in, out)
	})

	t.Run("rewrite legacy name for v0.8.0-v0.8.3", func(t *testing.T) {
		const (
			lenPrefixedReactStateName         = "\x15" + stateGobNameV07
			lenPrefixedCompatName             = "\x15" + stateGobNameV080
			lenPrefixedStateSerializationName = "\x12stateSerialization"
		)

		in := []byte(lenPrefixedReactStateName + "xxx" + lenPrefixedStateSerializationName + "yyy")
		out := preprocessADKCheckpoint(append([]byte(nil), in...))
		assert.True(t, bytes.Contains(out, []byte(lenPrefixedCompatName)))
		assert.False(t, bytes.Contains(out, []byte(lenPrefixedReactStateName)))
	})
}

func (h *interruptTestToolsHandler) BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) {
	runCtx.Tools = append(runCtx.Tools, h.tools...)
	return ctx, runCtx, nil
}

func TestSaveAgentEventWrapper(t *testing.T) {
	sr, sw := schema.Pipe[Message](1)
	sw.Send(schema.UserMessage("test"), nil)
	sw.Close()
	sr = sr.Copy(2)[1]

	w := &agentEventWrapper{
		AgentEvent: &AgentEvent{
			Output: &AgentOutput{
				MessageOutput: &MessageVariant{
					IsStreaming:   true,
					MessageStream: sr,
				},
			},
			RunPath: []RunStep{
				{
					"a1",
				},
				{
					"a2",
				},
			},
		},
		mu:                  sync.Mutex{},
		concatenatedMessage: nil,
	}

	_, err := getMessageFromWrappedEvent(w)
	assert.NoError(t, err)

	buf, err := w.GobEncode()
	assert.NoError(t, err)
	assert.NoError(t, err)

	w1 := &agentEventWrapper{}
	err = w1.GobDecode(buf)
	assert.NoError(t, err)
}

func TestInterruptFunctionsPopulateInterruptContextsImmediately(t *testing.T) {
	ctx := context.Background()
	ctx, _ = initRunCtx(ctx, "TestAgent", &AgentInput{Messages: []Message{}})
	ctx = AppendAddressSegment(ctx, AddressSegmentAgent, "TestAgent")

	t.Run("Interrupt populates InterruptContexts", func(t *testing.T) {
		event := Interrupt(ctx, "test info")
		assert.NotNil(t, event.Action)
		assert.NotNil(t, event.Action.Interrupted)
		assert.NotNil(t, event.Action.Interrupted.InterruptContexts)
		assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts))
		assert.Equal(t, "test info", event.Action.Interrupted.InterruptContexts[0].Info)
		assert.True(t, event.Action.Interrupted.InterruptContexts[0].IsRootCause)
		assert.Equal(t, Address{
			{Type: AddressSegmentAgent, ID: "TestAgent"},
		}, event.Action.Interrupted.InterruptContexts[0].Address)
	})

	t.Run("StatefulInterrupt populates InterruptContexts", func(t *testing.T) {
		event := StatefulInterrupt(ctx, "stateful info", "my state")
		assert.NotNil(t, event.Action)
		assert.NotNil(t, event.Action.Interrupted)
		assert.NotNil(t, event.Action.Interrupted.InterruptContexts)
		assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts))
		assert.Equal(t, "stateful info", event.Action.Interrupted.InterruptContexts[0].Info)
		assert.True(t, event.Action.Interrupted.InterruptContexts[0].IsRootCause)
	})

	t.Run("CompositeInterrupt populates InterruptContexts with filtered parent chain", func(t *testing.T) {
		subCtx := AppendAddressSegment(ctx, AddressSegmentAgent, "SubAgent")
		subEvent := Interrupt(subCtx, "sub info")
		event := CompositeInterrupt(ctx, "composite info", "composite state", subEvent.Action.internalInterrupted)
		assert.NotNil(t, event.Action)
		assert.NotNil(t, event.Action.Interrupted)
		assert.NotNil(t, event.Action.Interrupted.InterruptContexts)
		assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts))

		rootCause := event.Action.Interrupted.InterruptContexts[0]
		assert.Equal(t, "sub info", rootCause.Info)
		assert.True(t, rootCause.IsRootCause)
		assert.Equal(t, Address{
			{Type: AddressSegmentAgent, ID: "TestAgent"},
			{Type: AddressSegmentAgent, ID: "SubAgent"},
		}, rootCause.Address)

		assert.NotNil(t, rootCause.Parent, "Parent should not be nil for composite interrupt")
		assert.Equal(t, "composite info", rootCause.Parent.Info)
		assert.Equal(t, Address{
			{Type: AddressSegmentAgent, ID: "TestAgent"},
		}, rootCause.Parent.Address)
	})

	t.Run("Address only contains agent/tool segments", func(t *testing.T) {
		event := Interrupt(ctx, "test info")
		addr := event.Action.Interrupted.InterruptContexts[0].Address
		for _, seg := range addr {
			assert.True(t, seg.Type == AddressSegmentAgent || seg.Type == AddressSegmentTool,
				"Address should only contain agent/tool segments, got: %s", seg.Type)
		}
	})
}

func TestSimpleInterrupt(t *testing.T) {
	data := "hello world"
	agent := &myAgent{
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Send(&AgentEvent{
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						IsStreaming: true,
						Message:     nil,
						MessageStream: schema.StreamReaderFromArray([]Message{
							schema.UserMessage("hello "),
							schema.UserMessage("world"),
						}),
					},
				},
			})
			intEvent := Interrupt(ctx, data)
			intEvent.Action.Interrupted.Data = data
			generator.Send(intEvent)
			generator.Close()
			return iter
		},
		resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			assert.True(t, info.WasInterrupted)
			assert.Nil(t, info.InterruptState)
			assert.True(t, info.EnableStreaming)
			assert.Equal(t, data, info.Data)

			assert.True(t, info.IsResumeTarget)
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Close()
			return iter
		},
	}
	store := newMyStore()
	ctx := context.Background()
	runner := NewRunner(ctx, RunnerConfig{
		Agent:           agent,
		EnableStreaming: true,
		CheckPointStore: store,
	})
	iter := runner.Query(ctx, "hello world", WithCheckPointID("1"))
	_, ok := iter.Next()
	assert.True(t, ok)
	interruptEvent, ok := iter.Next()
	assert.True(t, ok)
	assert.Equal(t, data, interruptEvent.Action.Interrupted.Data)
	assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts[0].ID)
	assert.True(t, interruptEvent.Action.Interrupted.InterruptContexts[0].IsRootCause)
	assert.Equal(t, data, interruptEvent.Action.Interrupted.InterruptContexts[0].Info)
	assert.Equal(t, Address{{Type: AddressSegmentAgent, ID: "myAgent"}},
		interruptEvent.Action.Interrupted.InterruptContexts[0].Address)
	_, ok = iter.Next()
	assert.False(t, ok)

	iter, err := runner.ResumeWithParams(ctx, "1", &ResumeParams{
		Targets: map[string]any{
			interruptEvent.Action.Interrupted.InterruptContexts[0].ID: nil,
		},
	})
	assert.NoError(t, err)
	_, ok = iter.Next()
	assert.False(t, ok)
}

func TestMultiAgentInterrupt(t *testing.T) {
	ctx := context.Background()
	sa1 := &myAgent{
		name: "sa1",
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Send(&AgentEvent{
				AgentName: "sa1",
				Action: &AgentAction{
					TransferToAgent: &TransferToAgentAction{
						DestAgentName: "sa2",
					},
				},
			})
			generator.Close()
			return iter
		},
	}
	sa2 := &myAgent{
		name: "sa2",
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			intEvent := StatefulInterrupt(ctx, "hello world", "temp state")
			intEvent.Action.Interrupted.Data = "hello world"
			generator.Send(intEvent)
			generator.Close()
			return iter
		},
		resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			assert.NotNil(t, info)
			assert.Equal(t, info.Data, "hello world")

			assert.True(t, info.WasInterrupted)
			assert.NotNil(t, info.InterruptState)
			assert.Equal(t, "temp state", info.InterruptState)

			assert.True(t, info.IsResumeTarget)
			assert.NotNil(t, info.ResumeData)
			assert.Equal(t, "resume data", info.ResumeData)

			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Send(&AgentEvent{
				AgentName: "sa2",
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{Message: schema.UserMessage(info.ResumeData.(string))},
				},
			})
			generator.Close()
			return iter
		},
	}
	a, err := SetSubAgents(ctx, sa1, []Agent{sa2})
	assert.NoError(t, err)
	runner := NewRunner(ctx, RunnerConfig{
		Agent:           a,
		EnableStreaming: false,
		CheckPointStore: newMyStore(),
	})
	iter := runner.Query(ctx, "", WithCheckPointID("1"))
	event, ok := iter.Next()
	assert.True(t, ok)
	assert.NotNil(t, event.Action.TransferToAgent)
	event, ok = iter.Next()
	assert.True(t, ok)
	assert.NotNil(t, event.Action.Interrupted)
	assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts))
	assert.Equal(t, "hello world", event.Action.Interrupted.InterruptContexts[0].Info)
	assert.True(t, event.Action.Interrupted.InterruptContexts[0].IsRootCause)
	assert.Equal(t, Address{
		{Type: AddressSegmentAgent, ID: "sa1"},
		{Type: AddressSegmentAgent, ID: "sa2"},
	}, event.Action.Interrupted.InterruptContexts[0].Address)
	assert.NotEmpty(t, event.Action.Interrupted.InterruptContexts[0].ID)

	interruptID := event.Action.Interrupted.InterruptContexts[0].ID
	_, ok = iter.Next()
	assert.False(t, ok)

	iter, err = runner.ResumeWithParams(ctx, "1", &ResumeParams{
		Targets: map[string]any{
			interruptID: "resume data",
		},
	})
	assert.NoError(t, err)
	event, ok = iter.Next()
	assert.True(t, ok)
	assert.Equal(t, event.Output.MessageOutput.Message.Content, "resume data")
	_, ok = iter.Next()
	assert.False(t, ok)
}

func TestWorkflowInterrupt(t *testing.T) {
	ctx := context.Background()
	sa1 := &myAgent{
		name: "sa1",
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()

			intEvent := Interrupt(ctx, "sa1 interrupt data")
			intEvent.Action.Interrupted.Data = "sa1 interrupt data"
			generator.Send(intEvent)
			generator.Close()
			return iter
		},
		resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			assert.Equal(t, info.InterruptInfo.Data, "sa1 interrupt data")
			assert.True(t, info.WasInterrupted)
			assert.Nil(t, info.InterruptState)
			assert.True(t, info.IsResumeTarget)
			assert.Equal(t, "resume sa1", info.ResumeData)
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Close()
			return iter
		},
	} // interrupt once
	sa2 := &myAgent{
		name: "sa2",
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()

			intEvent := StatefulInterrupt(ctx, "sa2 interrupt data", "sa2 interrupt")
			intEvent.Action.Interrupted.Data = "sa2 interrupt data"
			generator.Send(intEvent)
			generator.Close()
			return iter
		},
		resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			assert.Equal(t, info.InterruptInfo.Data, "sa2 interrupt data")
			assert.True(t, info.WasInterrupted)
			assert.NotNil(t, info.InterruptState)
			assert.Equal(t, "sa2 interrupt", info.InterruptState)

			assert.True(t, info.IsResumeTarget)
			assert.NotNil(t, info.ResumeData)
			assert.Equal(t, "resume sa2", info.ResumeData)
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Close()
			return iter
		},
	} // interrupt once
	sa3 := &myAgent{
		name: "sa3",
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Send(&AgentEvent{
				AgentName: "sa3",
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						Message: schema.UserMessage("sa3 completed"),
					},
				},
			})
			generator.Close()
			return iter
		},
	} // won't interrupt
	sa4 := &myAgent{
		name: "sa4",
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Send(&AgentEvent{
				AgentName: "sa4",
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						Message: schema.UserMessage("sa4 completed"),
					},
				},
			})
			generator.Close()
			return iter
		},
	} // won't interrupt

	firstInterruptEvent := &AgentEvent{
		AgentName: "sa1",
		RunPath:   []RunStep{{"sequential"}, {"sa1"}},
		Action: &AgentAction{
			Interrupted: &InterruptInfo{
				Data: &WorkflowInterruptInfo{
					OrigInput: &AgentInput{
						Messages: []Message{schema.UserMessage("hello world")},
					},
					SequentialInterruptIndex: 0,
					SequentialInterruptInfo: &InterruptInfo{
						Data: "sa1 interrupt data",
					},
					LoopIterations: 0,
				},
				InterruptContexts: []*InterruptCtx{
					{
						ID:   "agent:sequential;agent:sa1",
						Info: "sa1 interrupt data",
						Address: Address{
							{
								ID:   "sequential",
								Type: AddressSegmentAgent,
							},
							{
								ID:   "sa1",
								Type: AddressSegmentAgent,
							},
						},
						IsRootCause: true,
						Parent: &InterruptCtx{
							ID:   "agent:sequential",
							Info: "Sequential workflow interrupted",
							Address: Address{
								{
									ID:   "sequential",
									Type: AddressSegmentAgent,
								},
							},
						},
					},
				},
			},
		},
	}
	_ = firstInterruptEvent
	secondInterruptEvent := &AgentEvent{
		AgentName: "sa2",
		RunPath:   []RunStep{{"sequential"}, {"sa1"}, {"sa2"}},
		Action: &AgentAction{
			Interrupted: &InterruptInfo{
				Data: &WorkflowInterruptInfo{
					OrigInput: &AgentInput{
						Messages: []Message{schema.UserMessage("hello world")},
					},
					SequentialInterruptIndex: 1,
					SequentialInterruptInfo: &InterruptInfo{
						Data: "sa2 interrupt data",
					},
				},
				InterruptContexts: []*InterruptCtx{
					{
						ID:   "agent:sequential;agent:sa1;agent:sa2",
						Info: "sa2 interrupt data",
						Address: Address{
							{
								ID:   "sequential",
								Type: AddressSegmentAgent,
							},
							{
								ID:   "sa2",
								Type: AddressSegmentAgent,
							},
						},
						IsRootCause: true,
						Parent: &InterruptCtx{
							ID:   "agent:sequential",
							Info: "Sequential workflow interrupted",
							Address: Address{
								{
									ID:   "sequential",
									Type: AddressSegmentAgent,
								},
							},
						},
					},
				},
			},
		},
	}
	_ = secondInterruptEvent
	messageEvents := []*AgentEvent{
		{
			AgentName: "sa3",
			RunPath:   []RunStep{{"sequential"}, {"sa1"}, {"sa2"}, {"sa3"}},
			Output: &AgentOutput{
				MessageOutput: &MessageVariant{
					Message: schema.UserMessage("sa3 completed"),
				},
			},
		},
		{
			AgentName: "sa4",
			RunPath:   []RunStep{{"sequential"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}},
			Output: &AgentOutput{
				MessageOutput: &MessageVariant{
					Message: schema.UserMessage("sa4 completed"),
				},
			},
		},
	}
	_ = messageEvents

	t.Run("test sequential workflow agent", func(t *testing.T) {

		// sequential
		a, err := NewSequentialAgent(ctx, &SequentialAgentConfig{
			Name:        "sequential",
			Description: "sequential agent",
			SubAgents:   []Agent{sa1, sa2, sa3, sa4},
		})
		assert.NoError(t, err)
		runner := NewRunner(ctx, RunnerConfig{
			Agent:           a,
			CheckPointStore: newMyStore(),
		})
		var events []*AgentEvent
		iter := runner.Query(ctx, "hello world", WithCheckPointID("sequential-1"))
		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			events = append(events, event)
		}

		assert.Equal(t, 1, len(events))
		assert.Equal(t, firstInterruptEvent.AgentName, events[0].AgentName)
		assert.Equal(t, firstInterruptEvent.RunPath, events[0].RunPath)
		assert.True(t, events[0].Action.Interrupted.InterruptContexts[0].EqualsWithoutID(firstInterruptEvent.Action.Interrupted.InterruptContexts[0]))
		interruptID1 := events[0].Action.Interrupted.InterruptContexts[0].ID
		events = []*AgentEvent{}

		// Resume after sa1 interrupt
		iter, err = runner.ResumeWithParams(ctx, "sequential-1", &ResumeParams{
			Targets: map[string]any{
				interruptID1: "resume sa1",
			},
		})
		assert.NoError(t, err)
		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			events = append(events, event)
		}

		assert.Equal(t, 1, len(events))
		assert.Equal(t, secondInterruptEvent.AgentName, events[0].AgentName)
		assert.Equal(t, secondInterruptEvent.RunPath, events[0].RunPath)
		assert.True(t, events[0].Action.Interrupted.InterruptContexts[0].
			EqualsWithoutID(secondInterruptEvent.Action.Interrupted.InterruptContexts[0]))
		interruptID2 := events[0].Action.Interrupted.InterruptContexts[0].ID
		events = []*AgentEvent{}

		// Resume after sa2 interrupt
		iter, err = runner.ResumeWithParams(ctx, "sequential-1", &ResumeParams{
			Targets: map[string]any{
				interruptID2: "resume sa2",
			},
		})
		assert.NoError(t, err)
		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			events = append(events, event)
		}

		assert.Equal(t, 2, len(events))
		assert.Equal(t, messageEvents, events)
	})

	t.Run("test loop workflow agent", func(t *testing.T) {
		// loop
		a, err := NewLoopAgent(ctx, &LoopAgentConfig{
			Name:          "loop",
			SubAgents:     []Agent{sa1, sa2, sa3, sa4},
			MaxIterations: 2,
		})
		assert.NoError(t, err)
		runner := NewRunner(ctx, RunnerConfig{
			Agent:           a,
			CheckPointStore: newMyStore(),
		})
		var events []*AgentEvent
		iter := runner.Query(ctx, "hello world", WithCheckPointID("loop-1"))
		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			events = append(events, event)
		}

		loopFirstInterruptEvent := &AgentEvent{
			AgentName: "sa1",
			RunPath:   []RunStep{{"loop"}, {"sa1"}},
			Action: &AgentAction{
				Interrupted: &InterruptInfo{
					Data: &WorkflowInterruptInfo{
						OrigInput: &AgentInput{
							Messages: []Message{schema.UserMessage("hello world")},
						},
						SequentialInterruptIndex: 0,
						SequentialInterruptInfo: &InterruptInfo{
							Data: "sa1 interrupt data",
						},
						LoopIterations: 0,
					},
					InterruptContexts: []*InterruptCtx{
						{
							ID:   "agent:loop;agent:sa1",
							Info: "sa1 interrupt data",
							Address: Address{
								{
									ID:   "loop",
									Type: AddressSegmentAgent,
								},
								{
									ID:   "sa1",
									Type: AddressSegmentAgent,
								},
							},
							IsRootCause: true,
							Parent: &InterruptCtx{
								ID:   "agent:loop",
								Info: "Loop workflow interrupted",
								Address: Address{
									{
										ID:   "loop",
										Type: AddressSegmentAgent,
									},
								},
							},
						},
					},
				},
			},
		}
		assert.Equal(t, 1, len(events))
		assert.Equal(t, loopFirstInterruptEvent.AgentName, events[0].AgentName)
		assert.Equal(t, loopFirstInterruptEvent.RunPath, events[0].RunPath)
		assert.True(t, events[0].Action.Interrupted.InterruptContexts[0].EqualsWithoutID(loopFirstInterruptEvent.Action.Interrupted.InterruptContexts[0]))
		loopInterruptID1 := events[0].Action.Interrupted.InterruptContexts[0].ID
		events = []*AgentEvent{}

		// Resume after sa1 interrupt
		iter, err = runner.ResumeWithParams(ctx, "loop-1", &ResumeParams{
			Targets: map[string]any{
				loopInterruptID1: "resume sa1",
			},
		})
		assert.NoError(t, err)
		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			events = append(events, event)
		}

		loopSecondInterruptEvent := &AgentEvent{
			AgentName: "sa2",
			RunPath:   []RunStep{{"loop"}, {"sa1"}, {"sa2"}},
			Action: &AgentAction{
				Interrupted: &InterruptInfo{
					Data: &WorkflowInterruptInfo{
						OrigInput: &AgentInput{
							Messages: []Message{schema.UserMessage("hello world")},
						},
						SequentialInterruptIndex: 1,
						SequentialInterruptInfo: &InterruptInfo{
							Data: "sa2 interrupt data",
						},
						LoopIterations: 0,
					},
					InterruptContexts: []*InterruptCtx{
						{
							ID:   "agent:loop;agent:sa1;agent:sa2",
							Info: "sa2 interrupt data",
							Address: Address{
								{
									ID:   "loop",
									Type: AddressSegmentAgent,
								},
								{
									ID:   "sa2",
									Type: AddressSegmentAgent,
								},
							},
							IsRootCause: true,
							Parent: &InterruptCtx{
								ID:   "agent:loop",
								Info: "Loop workflow interrupted",
								Address: Address{
									{
										ID:   "loop",
										Type: AddressSegmentAgent,
									},
								},
							},
						},
					},
				},
			},
		}
		assert.Equal(t, 1, len(events))
		assert.Equal(t, loopSecondInterruptEvent.AgentName, events[0].AgentName)
		assert.Equal(t, loopSecondInterruptEvent.RunPath, events[0].RunPath)
		assert.True(t, events[0].Action.Interrupted.InterruptContexts[0].EqualsWithoutID(loopSecondInterruptEvent.Action.Interrupted.InterruptContexts[0]))
		loopInterruptID2 := events[0].Action.Interrupted.InterruptContexts[0].ID
		events = []*AgentEvent{}

		// Resume after sa2 interrupt
		iter, err = runner.ResumeWithParams(ctx, "loop-1", &ResumeParams{
			Targets: map[string]any{
				loopInterruptID2: "resume sa2",
			},
		})
		assert.NoError(t, err)
		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			events = append(events, event)
		}

		loopThirdInterruptEvent := &AgentEvent{
			AgentName: "sa1",
			RunPath:   []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}, {"sa1"}},
			Action: &AgentAction{
				Interrupted: &InterruptInfo{
					Data: &WorkflowInterruptInfo{
						OrigInput: &AgentInput{
							Messages: []Message{schema.UserMessage("hello world")},
						},
						SequentialInterruptIndex: 0,
						SequentialInterruptInfo: &InterruptInfo{
							Data: "sa1 interrupt data",
						},
						LoopIterations: 1,
					},
					InterruptContexts: []*InterruptCtx{
						{
							ID:   "agent:loop;agent:sa1;agent:sa2;agent:sa3;agent:sa4;agent:sa1",
							Info: "sa1 interrupt data",
							Address: Address{
								{
									ID:   "loop",
									Type: AddressSegmentAgent,
								},
								{
									ID:   "sa1",
									Type: AddressSegmentAgent,
								},
							},
							IsRootCause: true,
							Parent: &InterruptCtx{
								ID:   "agent:loop",
								Info: "Loop workflow interrupted",
								Address: Address{
									{
										ID:   "loop",
										Type: AddressSegmentAgent,
									},
								},
							},
						},
					},
				},
			},
		}

		loopFourthInterruptEvent := &AgentEvent{
			AgentName: "sa2",
			RunPath:   []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}, {"sa1"}, {"sa2"}},
			Action: &AgentAction{
				Interrupted: &InterruptInfo{
					Data: &WorkflowInterruptInfo{
						OrigInput: &AgentInput{
							Messages: []Message{schema.UserMessage("hello world")},
						},
						SequentialInterruptIndex: 1,
						SequentialInterruptInfo: &InterruptInfo{
							Data: "sa2 interrupt data",
						},
						LoopIterations: 1,
					},
					InterruptContexts: []*InterruptCtx{
						{
							ID:   "agent:loop;agent:sa1;agent:sa2;agent:sa3;agent:sa4;agent:sa1;agent:sa2",
							Info: "sa2 interrupt data",
							Address: Address{
								{
									ID:   "loop",
									Type: AddressSegmentAgent,
								},
								{
									ID:   "sa2",
									Type: AddressSegmentAgent,
								},
							},
							IsRootCause: true,
							Parent: &InterruptCtx{
								ID:   "agent:loop",
								Info: "Loop workflow interrupted",
								Address: Address{
									{
										ID:   "loop",
										Type: AddressSegmentAgent,
									},
								},
							},
						},
					},
				},
			},
		}

		loopMessageEvents := []*AgentEvent{
			{
				AgentName: "sa3",
				RunPath:   []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}},
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						Message: schema.UserMessage("sa3 completed"),
					},
				},
			},
			{
				AgentName: "sa4",
				RunPath:   []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}},
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						Message: schema.UserMessage("sa4 completed"),
					},
				},
			},
			loopThirdInterruptEvent,
		}
		assert.Equal(t, 3, len(events))
		// Check the first two message events
		assert.Equal(t, loopMessageEvents[0].AgentName, events[0].AgentName)
		assert.Equal(t, loopMessageEvents[0].RunPath, events[0].RunPath)
		assert.Equal(t, loopMessageEvents[0].Output.MessageOutput.Message.Content, events[0].Output.MessageOutput.Message.Content)

		assert.Equal(t, loopMessageEvents[1].AgentName, events[1].AgentName)
		assert.Equal(t, loopMessageEvents[1].RunPath, events[1].RunPath)
		assert.Equal(t, loopMessageEvents[1].Output.MessageOutput.Message.Content, events[1].Output.MessageOutput.Message.Content)

		// Check the third interrupt event using EqualsWithoutID
		assert.Equal(t, loopMessageEvents[2].AgentName, events[2].AgentName)
		assert.Equal(t, loopMessageEvents[2].RunPath, events[2].RunPath)
		assert.True(t, events[2].Action.Interrupted.InterruptContexts[0].EqualsWithoutID(loopMessageEvents[2].Action.Interrupted.InterruptContexts[0]))
		loopInterruptID3 := events[2].Action.Interrupted.InterruptContexts[0].ID
		events = []*AgentEvent{}

		// Resume after third interrupt
		iter, err = runner.ResumeWithParams(ctx, "loop-1", &ResumeParams{
			Targets: map[string]any{
				loopInterruptID3: "resume sa1",
			},
		})
		assert.NoError(t, err)
		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			events = append(events, event)
		}
		assert.Equal(t, 1, len(events))
		assert.Equal(t, loopFourthInterruptEvent.AgentName, events[0].AgentName)
		assert.Equal(t, loopFourthInterruptEvent.RunPath, events[0].RunPath)
		assert.True(t, events[0].Action.Interrupted.InterruptContexts[0].EqualsWithoutID(loopFourthInterruptEvent.Action.Interrupted.InterruptContexts[0]))
		loopInterruptID4 := events[0].Action.Interrupted.InterruptContexts[0].ID
		events = []*AgentEvent{}

		// Resume after fourth interrupt
		iter, err = runner.ResumeWithParams(ctx, "loop-1", &ResumeParams{
			Targets: map[string]any{
				loopInterruptID4: "resume sa2",
			},
		})
		assert.NoError(t, err)
		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			events = append(events, event)
		}
		loopFinalMessageEvents := []*AgentEvent{
			{
				AgentName: "sa3",
				RunPath:   []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}, {"sa1"}, {"sa2"}, {"sa3"}},
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						Message: schema.UserMessage("sa3 completed"),
					},
				},
			},
			{
				AgentName: "sa4",
				RunPath:   []RunStep{{"loop"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}, {"sa1"}, {"sa2"}, {"sa3"}, {"sa4"}},
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						Message: schema.UserMessage("sa4 completed"),
					},
				},
			},
		}
		assert.Equal(t, 2, len(events))
		assert.Equal(t, loopFinalMessageEvents, events)
	})

	t.Run("test parallel workflow agent", func(t *testing.T) {
		// parallel
		a, err := NewParallelAgent(ctx, &ParallelAgentConfig{
			Name:      "parallel agent",
			SubAgents: []Agent{sa1, sa2, sa3, sa4},
		})
		assert.NoError(t, err)
		runner := NewRunner(ctx, RunnerConfig{
			Agent:           a,
			CheckPointStore: newMyStore(),
		})
		iter := runner.Query(ctx, "hello world", WithCheckPointID("1"))
		var (
			events         []*AgentEvent
			interruptEvent *AgentEvent
		)

		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			if event.Action != nil && event.Action.Interrupted != nil {
				interruptEvent = event
				continue
			}
			events = append(events, event)
		}
		assert.Equal(t, 2, len(events))

		// Debug: Print actual events to see what we're getting
		for i, event := range events {
			t.Logf("Event %d: AgentName=%s, RunPath=%v, Output=%v", i, event.AgentName, event.RunPath, event.Output)
		}

		// Define parallel message events separately
		parallelMessageEvents := []*AgentEvent{
			{
				AgentName: "sa4",
				RunPath:   []RunStep{{"parallel agent"}, {"sa4"}},
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						Message: schema.UserMessage("sa4 completed"),
					},
				},
			},
			{
				AgentName: "sa3",
				RunPath:   []RunStep{{"parallel agent"}, {"sa3"}},
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{
						Message: schema.UserMessage("sa3 completed"),
					},
				},
			},
		}

		assert.Contains(t, events, parallelMessageEvents[0])
		assert.Contains(t, events, parallelMessageEvents[1])

		assert.NotNil(t, interruptEvent)
		assert.Equal(t, "parallel agent", interruptEvent.AgentName)
		assert.Equal(t, []RunStep{{"parallel agent"}}, interruptEvent.RunPath)
		assert.NotNil(t, interruptEvent.Action.Interrupted)
		wii, ok := interruptEvent.Action.Interrupted.Data.(*WorkflowInterruptInfo)
		assert.True(t, ok)
		assert.Equal(t, 2, len(wii.ParallelInterruptInfo))

		var sa1Found, sa2Found bool
		for _, info := range wii.ParallelInterruptInfo {
			switch info.Data {
			case "sa1 interrupt data":
				sa1Found = true
			case "sa2 interrupt data":
				sa2Found = true
			}
		}
		assert.True(t, sa1Found)
		assert.True(t, sa2Found)

		var sa1InfoFound, sa2InfoFound bool
		for _, ctx := range interruptEvent.Action.Interrupted.InterruptContexts {
			switch ctx.Info {
			case "sa1 interrupt data":
				sa1InfoFound = true
			case "sa2 interrupt data":
				sa2InfoFound = true
			}
		}

		assert.Equal(t, 2, len(interruptEvent.Action.Interrupted.InterruptContexts))
		assert.True(t, sa1InfoFound)
		assert.True(t, sa2InfoFound)

		var parallelInterruptID1, parallelInterruptID2 string
		for _, ctx := range interruptEvent.Action.Interrupted.InterruptContexts {
			switch ctx.Info {
			case "sa1 interrupt data":
				parallelInterruptID1 = ctx.ID
			case "sa2 interrupt data":
				parallelInterruptID2 = ctx.ID
			}
		}
		assert.NotEmpty(t, parallelInterruptID1)
		assert.NotEmpty(t, parallelInterruptID2)

		iter, err = runner.ResumeWithParams(ctx, "1", &ResumeParams{
			Targets: map[string]any{
				parallelInterruptID1: "resume sa1",
				parallelInterruptID2: "resume sa2",
			},
		})
		assert.NoError(t, err)
		_, ok = iter.Next()
		assert.False(t, ok)
	})
}

func TestChatModelInterrupt(t *testing.T) {
	ctx := context.Background()
	a, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "name",
		Description: "description",
		Instruction: "instruction",
		Model: &myModel{
			validator: func(i int, messages []*schema.Message) bool {
				if i > 0 && (len(messages) != 4 || messages[2].Content != "new user message") {
					return false
				}
				return true
			},
			messages: []*schema.Message{
				schema.AssistantMessage("", []schema.ToolCall{
					{
						ID: "1",
						Function: schema.FunctionCall{
							Name:      "tool1",
							Arguments: "arguments",
						},
					},
				}),
				schema.AssistantMessage("completed", nil),
			},
		},
		ToolsConfig: ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{&myTool1{}},
			},
		},
	})
	assert.NoError(t, err)
	runner := NewRunner(ctx, RunnerConfig{
		Agent:           a,
		CheckPointStore: newMyStore(),
	})
	iter := runner.Query(ctx, "hello world", WithCheckPointID("1"))
	_, ok := iter.Next()
	assert.True(t, ok)
	event, ok := iter.Next()
	assert.True(t, ok)
	assert.NoError(t, event.Err)
	assert.NotNil(t, event.Action.Interrupted)
	assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts))
	assert.Equal(t, Address{
		{Type: AddressSegmentAgent, ID: "name"},
		{Type: AddressSegmentTool, ID: "tool1", SubID: "1"},
	}, event.Action.Interrupted.InterruptContexts[0].Address)

	var (
		chatModelAgentID string
		toolID           string
	)

	intCtx := event.Action.Interrupted.InterruptContexts[0]
	for intCtx != nil {
		if intCtx.Address[len(intCtx.Address)-1].Type == AddressSegmentTool {
			toolID = intCtx.ID
		} else if intCtx.Address[len(intCtx.Address)-1].Type == AddressSegmentAgent {
			chatModelAgentID = intCtx.ID
		}
		intCtx = intCtx.Parent
	}

	_, ok = iter.Next()
	assert.False(t, ok)

	iter, err = runner.ResumeWithParams(ctx, "1", &ResumeParams{
		Targets: map[string]any{
			chatModelAgentID: &ChatModelAgentResumeData{
				HistoryModifier: func(ctx context.Context, history []Message) []Message {
					history[2].Content = "new user message"
					return history
				},
			},
			toolID: "tool resume result",
		},
	})
	assert.NoError(t, err)
	event, ok = iter.Next()
	assert.True(t, ok)
	assert.NoError(t, event.Err)
	assert.Equal(t, event.Output.MessageOutput.Message.Content, "tool resume result")
	event, ok = iter.Next()
	assert.True(t, ok)
	assert.NoError(t, event.Err)
	assert.Equal(t, event.Output.MessageOutput.Message.Content, "completed")
}

func TestChatModelAgentToolInterrupt(t *testing.T) {
	sa := &myAgent{
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			intAct := Interrupt(ctx, "hello world")
			intAct.Action.Interrupted.Data = "hello world"
			generator.Send(intAct)
			generator.Close()
			return iter
		},
		resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			assert.NotNil(t, info)
			assert.False(t, info.EnableStreaming)

			if !info.IsResumeTarget {
				iter, generator := NewAsyncIteratorPair[*AgentEvent]()
				intAct := Interrupt(ctx, "interrupt again")
				intAct.Action.Interrupted.Data = "interrupt again"
				generator.Send(intAct)
				generator.Close()
				return iter
			}

			assert.NotNil(t, info.ResumeData)
			assert.Equal(t, "resume sa", info.ResumeData)

			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Send(&AgentEvent{Output: &AgentOutput{MessageOutput: &MessageVariant{Message: schema.UserMessage(fmt.Sprintf("my agent completed with data %s", info.ResumeData))}}})
			generator.Close()
			return iter
		},
	}
	ctx := context.Background()
	a, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "name",
		Description: "description",
		Instruction: "instruction",
		Model: &myModel{
			messages: []*schema.Message{
				schema.AssistantMessage("", []schema.ToolCall{
					{
						ID: "1",
						Function: schema.FunctionCall{
							Name:      "myAgent",
							Arguments: "{\"request\":\"123\"}",
						},
					},
				}),
				schema.AssistantMessage("completed", nil),
			},
		},
		ToolsConfig: ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{NewAgentTool(ctx, sa)},
			},
		},
	})
	assert.NoError(t, err)
	runner := NewRunner(ctx, RunnerConfig{
		Agent:           a,
		CheckPointStore: newMyStore(),
	})

	iter := runner.Query(ctx, "hello world", WithCheckPointID("1"))
	_, ok := iter.Next()
	assert.True(t, ok)
	event, ok := iter.Next()
	assert.True(t, ok)
	assert.NoError(t, event.Err)
	assert.NotNil(t, event.Action.Interrupted)
	_, ok = iter.Next()
	assert.False(t, ok)

	iter, err = runner.Resume(ctx, "1")
	assert.NoError(t, err)
	event, ok = iter.Next()
	assert.True(t, ok)
	assert.NoError(t, event.Err)
	assert.NotNil(t, event.Action.Interrupted)
	assert.Equal(t, 1, len(event.Action.Interrupted.InterruptContexts))
	for _, ctx := range event.Action.Interrupted.InterruptContexts {
		if ctx.IsRootCause {
			assert.Equal(t, Address{
				{Type: AddressSegmentAgent, ID: "name"},
				{Type: AddressSegmentTool, ID: "myAgent", SubID: "1"},
				{Type: AddressSegmentAgent, ID: "myAgent"},
			}, ctx.Address)
			assert.Equal(t, "interrupt again", ctx.Info)
		}
	}

	var toolInterruptID string
	for _, ctx := range event.Action.Interrupted.InterruptContexts {
		if ctx.IsRootCause {
			toolInterruptID = ctx.ID
			break
		}
	}
	assert.NotEmpty(t, toolInterruptID)

	_, ok = iter.Next()
	assert.False(t, ok)

	iter, err = runner.ResumeWithParams(ctx, "1", &ResumeParams{
		Targets: map[string]any{
			toolInterruptID: "resume sa",
		},
	})
	assert.NoError(t, err)
	event, ok = iter.Next()
	assert.True(t, ok)
	assert.NoError(t, event.Err)
	assert.Equal(t, event.Output.MessageOutput.Message.Content, "my agent completed with data resume sa")
	event, ok = iter.Next()
	assert.True(t, ok)
	assert.NoError(t, event.Err)
	assert.Equal(t, event.Output.MessageOutput.Message.Content, "completed")
	_, ok = iter.Next()
	assert.False(t, ok)
}

func newMyStore() *myStore {
	return &myStore{
		m: map[string][]byte{},
	}
}

type myStore struct {
	m map[string][]byte
}

func (m *myStore) Set(_ context.Context, key string, value []byte) error {
	m.m[key] = value
	return nil
}

func (m *myStore) Get(_ context.Context, key string) ([]byte, bool, error) {
	v, ok := m.m[key]
	return v, ok, nil
}

type myAgentOptions struct {
	value string
}

func withValue(value string) AgentRunOption {
	return WrapImplSpecificOptFn(func(t *myAgentOptions) {
		t.value = value
	})
}

type myAgent struct {
	name     string
	runFn    func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent]
	resumeFn func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent]
}

func (m *myAgent) Name(_ context.Context) string {
	if len(m.name) > 0 {
		return m.name
	}
	return "myAgent"
}

func (m *myAgent) Description(_ context.Context) string {
	return "myAgent description"
}

func (m *myAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	return m.runFn(ctx, input, options...)
}

func (m *myAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	return m.resumeFn(ctx, info, opts...)
}

type myModel struct {
	times     int
	messages  []*schema.Message
	validator func(int, []*schema.Message) bool
}

func (m *myModel) Generate(_ context.Context, input []*schema.Message, _ ...model.Option) (*schema.Message, error) {
	if m.validator != nil && !m.validator(m.times, input) {
		return nil, errors.New("invalid input")
	}
	if m.times >= len(m.messages) {
		return nil, errors.New("exceeded max number of messages")
	}
	t := m.times
	m.times++
	return m.messages[t], nil
}

func (m *myModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
	panic("implement me")
}

func (m *myModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
	return m, nil
}

type myTool1 struct{}

func (m *myTool1) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: "tool1",
		Desc: "desc",
	}, nil
}

func (m *myTool1) InvokableRun(ctx context.Context, _ string, _ ...tool.Option) (string, error) {
	if wasInterrupted, _, _ := tool.GetInterruptState[any](ctx); !wasInterrupted {
		return "", tool.Interrupt(ctx, nil)
	}

	if isResumeFlow, hasResumeData, data := tool.GetResumeContext[string](ctx); !isResumeFlow {
		return "", tool.Interrupt(ctx, nil)
	} else if hasResumeData {
		return data, nil
	}

	return "result", nil
}

func TestCyclicalAgentInterrupt(t *testing.T) {
	ctx := context.Background()

	var agentA, agentB, agentC Agent

	// agentC interrupts
	agentC = &myAgent{
		name: "C",
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			intAct := Interrupt(ctx, "interrupt from C")
			generator.Send(intAct)
			generator.Close()
			return iter
		},
		resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			assert.True(t, info.IsResumeTarget)
			assert.NotNil(t, info.ResumeData)
			assert.Equal(t, "resume C", info.ResumeData)
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Send(&AgentEvent{
				AgentName: "C",
				Output: &AgentOutput{
					MessageOutput: &MessageVariant{Message: schema.UserMessage("C completed")},
				},
			})
			generator.Close()
			return iter
		},
	}

	// agentB transfers back to its parent A
	agentB = &myAgent{
		name: "B",
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()
			generator.Send(&AgentEvent{
				AgentName: "B",
				Action: &AgentAction{
					TransferToAgent: &TransferToAgentAction{
						DestAgentName: "A", // Transfer back to parent
					},
				},
			})
			generator.Close()
			return iter
		},
	}

	// agentA is the parent, orchestrating the A->B->A->C flow
	agentA = &myAgent{
		name: "A",
		runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
			runCtx := getRunCtx(ctx)
			iter, generator := NewAsyncIteratorPair[*AgentEvent]()

			// If the last agent was B, we are in the A->B->A path, so transfer to C.
			// Otherwise, it's the first run, transfer to B.
			dest := "B"
			if len(runCtx.RunPath) > 1 && runCtx.RunPath[len(runCtx.RunPath)-2].agentName == "B" {
				dest = "C"
			}

			generator.Send(&AgentEvent{
				AgentName: "A",
				Action: &AgentAction{
					TransferToAgent: &TransferToAgentAction{
						DestAgentName: dest,
					},
				},
			})
			generator.Close()
			return iter
		},
	}

	// Set up the hierarchy: A is parent of B and C.
	agentA, err := SetSubAgents(ctx, agentA, []Agent{agentB, agentC})
	assert.NoError(t, err)

	// Run the test
	runner := NewRunner(ctx, RunnerConfig{
		Agent:           agentA,
		CheckPointStore: newMyStore(),
	})
	iter := runner.Query(ctx, "start", WithCheckPointID("cyclical-1"))

	var events []*AgentEvent
	for {
		event, ok := iter.Next()
		if !ok {
			break
		}
		events = append(events, event)
	}

	// We expect 3 transfer events (A->B, B->A, A->C) and 1 interrupt event from C.
	assert.Equal(t, 4, len(events))

	interruptEvent := events[3]
	assert.NotNil(t, interruptEvent.Action.Interrupted)
	assert.Equal(t, "C", interruptEvent.AgentName)

	// Check the interrupt context
	assert.Equal(t, 1, len(interruptEvent.Action.Interrupted.InterruptContexts))
	interruptCtx := interruptEvent.Action.Interrupted.InterruptContexts[0]
	assert.True(t, interruptCtx.IsRootCause)
	assert.Equal(t, "interrupt from C", interruptCtx.Info)

	expectedAddr := Address{
		{Type: AddressSegmentAgent, ID: "A"},
		{Type: AddressSegmentAgent, ID: "B"},
		{Type: AddressSegmentAgent, ID: "A"},
		{Type: AddressSegmentAgent, ID: "C"},
	}
	assert.Equal(t, expectedAddr, interruptCtx.Address)
	assert.NotEmpty(t, interruptCtx.ID)

	// Resume the execution
	iter, err = runner.ResumeWithParams(ctx, "cyclical-1", &ResumeParams{
		Targets: map[string]any{
			interruptCtx.ID: "resume C",
		},
	})
	assert.NoError(t, err)

	events = []*AgentEvent{}
	for {
		event, ok := iter.Next()
		if !ok {
			break
		}
		events = append(events, event)
	}

	// We expect one output event from C
	assert.Equal(t, 1, len(events))
	assert.Equal(t, "C completed", events[0].Output.MessageOutput.Message.Content)
}

// myStatefulTool is a tool that can interrupt and has internal state to track invocations.

type myStatefulTool struct {
	name string
	t    *testing.T
}

func (m *myStatefulTool) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: m.name,
		Desc: "desc",
	}, nil
}

type myStatefulToolState struct {
	InterruptCount int
}

func init() {
	schema.Register[myStatefulToolState]()
}

func (m *myStatefulTool) InvokableRun(ctx context.Context, _ string, _ ...tool.Option) (string, error) {
	wasInterrupted, hasState, state := tool.GetInterruptState[myStatefulToolState](ctx)
	if !wasInterrupted {
		return "", tool.StatefulInterrupt(ctx, fmt.Sprintf("interrupt from %s", m.name), myStatefulToolState{InterruptCount: 1})
	}

	isResumeFlow, hasResumeData, data := tool.GetResumeContext[string](ctx)
	if !isResumeFlow || !hasResumeData {
		assert.True(m.t, hasState, "tool %s should have interrupt state on resume", m.name)
		return "", tool.StatefulInterrupt(ctx, fmt.Sprintf("interrupt from %s", m.name), myStatefulToolState{InterruptCount: state.InterruptCount + 1})
	}

	return data, nil
}

func TestChatModelParallelToolInterruptAndResume(t *testing.T) {
	ctx := context.Background()

	toolA := &myStatefulTool{name: "toolA", t: t}
	toolB := &myStatefulTool{name: "toolB", t: t}

	chatModel, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "ParallelToolAgent",
		Description: "An agent that uses parallel tools",
		Model: &myModel{
			messages: []*schema.Message{
				// 1. First model response: call toolA and toolB in parallel
				schema.AssistantMessage("", []schema.ToolCall{
					{ID: "1", Function: schema.FunctionCall{Name: "toolA", Arguments: "{}"}},
					{ID: "2", Function: schema.FunctionCall{Name: "toolB", Arguments: "{}"}},
				}),
				// 2. Second model response (after tools are resumed): call them again to check state
				schema.AssistantMessage("", []schema.ToolCall{
					{ID: "3", Function: schema.FunctionCall{Name: "toolA", Arguments: "{}"}},
					{ID: "4", Function: schema.FunctionCall{Name: "toolB", Arguments: "{}"}},
				}),
				// 3. Final completion
				schema.AssistantMessage("all done", nil),
			},
		},
		ToolsConfig: ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{toolA, toolB},
			},
		},
	})
	assert.NoError(t, err)

	runner := NewRunner(ctx, RunnerConfig{
		Agent:           chatModel,
		CheckPointStore: newMyStore(),
	})

	// 1. Initial query -> parallel interrupt from toolA and toolB
	iter := runner.Query(ctx, "start", WithCheckPointID("parallel-tool-test-1"))
	normalEvents, interruptEvent := consumeUntilInterrupt(iter)

	assert.Equal(t, 1, len(normalEvents))
	assert.NotNil(t, interruptEvent)
	assert.Equal(t, 2, len(interruptEvent.Action.Interrupted.InterruptContexts),
		"should have 2 interrupts")

	var toolAInterruptID, toolBInterruptID string
	for _, info := range interruptEvent.Action.Interrupted.InterruptContexts {
		switch info.Info {
		case "interrupt from toolA":
			toolAInterruptID = info.ID
			assert.True(t, info.IsRootCause)
		case "interrupt from toolB":
			toolBInterruptID = info.ID
			assert.True(t, info.IsRootCause)
		}
	}
	assert.NotEmpty(t, toolAInterruptID)
	assert.NotEmpty(t, toolBInterruptID)

	// 2. Resume, targeting only toolA. toolB should re-interrupt.
	iter, err = runner.ResumeWithParams(ctx, "parallel-tool-test-1", &ResumeParams{
		Targets: map[string]any{
			toolAInterruptID: "toolA resumed",
		},
	})
	assert.NoError(t, err)
	_, interruptEvent = consumeUntilInterrupt(iter)

	assert.NotNil(t, interruptEvent, "expected a re-interrupt from toolB")
	assert.Equal(t, 1, len(interruptEvent.Action.Interrupted.InterruptContexts),
		"should have 1 remaining interrupts")

	var rootCause *InterruptCtx
	for _, info := range interruptEvent.Action.Interrupted.InterruptContexts {
		if info.IsRootCause {
			rootCause = info
			break
		}
	}

	if rootCause == nil {
		t.Fatal("expected a root cause interrupt from toolB")
	}
	assert.Equal(t, "interrupt from toolB", rootCause.Info)
	toolBReInterruptID := rootCause.ID

	// 3. Resume the re-interrupted toolB. The agent should then call the tools again.
	iter, err = runner.ResumeWithParams(ctx, "parallel-tool-test-1", &ResumeParams{
		Targets: map[string]any{
			toolBReInterruptID: "toolB resumed",
		},
	})
	assert.NoError(t, err)

	// 4. Consume all final events. The internal assertions in the tools will check the wasInterrupted flag.
	// We expect to see the results of the second tool calls, and then the final agent completion.
	finalEvents, interruptEvent := consumeUntilInterrupt(iter)
	assert.Equal(t, 2, len(finalEvents))
	assert.NotNil(t, interruptEvent)
}

// TestNestedChatModelAgentWithAgentTool verifies that the shouldFire method correctly prevents
// duplicate event firing in nested ChatModelAgent scenarios (ChatModelAgent -> AgentTool -> ChatModelAgent).
// This ensures that only the inner agent's cbHandler fires, not the outer agent's.
func TestNestedChatModelAgentWithAgentTool(t *testing.T) {
	ctx := context.Background()

	// Create an interruptible tool for the inner agent
	innerTool := &myStatefulTool{name: "innerTool", t: t}

	// Create the inner ChatModelAgent that will be wrapped by AgentTool
	innerAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "InnerAgent",
		Description: "Inner agent with interruptible tool",
		Model: &myModel{
			messages: []*schema.Message{
				schema.AssistantMessage("", []schema.ToolCall{
					{ID: "1", Function: schema.FunctionCall{Name: "innerTool", Arguments: "{}"}},
				}),
				schema.AssistantMessage("inner agent completed", nil),
			},
		},
		ToolsConfig: ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{innerTool},
			},
		},
	})
	assert.NoError(t, err)

	// Wrap the inner agent in an AgentTool
	agentTool := NewAgentTool(ctx, innerAgent)

	// Create the outer ChatModelAgent that uses the AgentTool
	outerAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "OuterAgent",
		Description: "Outer agent with AgentTool containing inner agent",
		Model: &myModel{
			messages: []*schema.Message{
				schema.AssistantMessage("", []schema.ToolCall{
					{ID: "1", Function: schema.FunctionCall{Name: "InnerAgent", Arguments: "{}"}},
				}),
				schema.AssistantMessage("outer agent completed", nil),
			},
		},
		ToolsConfig: ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{agentTool},
			},
		},
	})
	assert.NoError(t, err)

	runner := NewRunner(ctx, RunnerConfig{
		Agent:           outerAgent,
		CheckPointStore: newMyStore(),
	})

	// Run the query - this should trigger the nested agent structure
	iter := runner.Query(ctx, "start", WithCheckPointID("nested-agent-test-1"))

	// Collect all events to verify no duplicates
	var allEvents []*AgentEvent
	var interruptEvent *AgentEvent

	for {
		event, ok := iter.Next()
		if !ok {
			break
		}

		if event.Action != nil && event.Action.Interrupted != nil {
			assert.Nil(t, interruptEvent)
			interruptEvent = event
		}

		allEvents = append(allEvents, event)
	}

	if interruptEvent == nil {
		t.Fatal("expected an interrupt event")
	}

	// Verify we got exactly one interrupt event (not duplicated)
	assert.NotNil(t, interruptEvent, "should have an interrupt event")
	assert.Equal(t, 1, len(interruptEvent.Action.Interrupted.InterruptContexts),
		"should have exactly one interrupt context")

	// Verify the interrupt comes from the inner tool, not duplicated
	interruptCtx := interruptEvent.Action.Interrupted.InterruptContexts[0]
	assert.True(t, interruptCtx.IsRootCause, "interrupt should be root cause")
	assert.Equal(t, "interrupt from innerTool", interruptCtx.Info)

	// Verify the address path shows the correct nested structure
	expectedAddress := Address{
		{Type: AddressSegmentAgent, ID: "OuterAgent"},
		{Type: AddressSegmentTool, ID: "InnerAgent", SubID: "1"},
		{Type: AddressSegmentAgent, ID: "InnerAgent"},
		{Type: AddressSegmentTool, ID: "innerTool", SubID: "1"},
	}
	assert.Equal(t, expectedAddress, interruptCtx.Address,
		"interrupt address should show correct nested structure")

	// Verify no duplicate events by checking agent names in events
	var agentNames []string
	for _, event := range allEvents {
		if event.AgentName != "" {
			agentNames = append(agentNames, event.AgentName)
		}
	}

	// Should only have events from the outer agent (the inner agent's events should be handled
	// by the AgentTool and not duplicated by the outer agent's cbHandler)
	for _, name := range agentNames {
		assert.Equal(t, "OuterAgent", name,
			"all events should come from OuterAgent, not duplicated from InnerAgent")
	}

	// Now resume the interrupt
	interruptID := interruptCtx.ID
	iter, err = runner.ResumeWithParams(ctx, "nested-agent-test-1", &ResumeParams{
		Targets: map[string]any{
			interruptID: "resume inner tool",
		},
	})
	assert.NoError(t, err)

	// Collect final events after resume
	var finalEvents []*AgentEvent
	for {
		event, ok := iter.Next()
		if !ok {
			break
		}
		finalEvents = append(finalEvents, event)
	}

	// Verify completion events
	assert.Greater(t, len(finalEvents), 0, "should have completion events after resume")

	// Check that we get the expected completion messages
	var foundInnerCompletion, foundOuterCompletion bool
	for _, event := range finalEvents {
		if event.Output != nil && event.Output.MessageOutput != nil {
			if event.Output.MessageOutput.Message != nil {
				content := event.Output.MessageOutput.Message.Content
				switch content {
				case "inner agent completed":
					foundInnerCompletion = true
				case "outer agent completed":
					foundOuterCompletion = true
				}
			}
		}
	}

	assert.True(t, foundInnerCompletion, "should have inner agent completion")
	assert.True(t, foundOuterCompletion, "should have outer agent completion")
}

// consumeUntilInterrupt consumes events from the iterator until an interrupt is found or it's exhausted.
func consumeUntilInterrupt(iter *AsyncIterator[*AgentEvent]) (normalEvents []*AgentEvent, interruptEvent *AgentEvent) {
	for {
		event, ok := iter.Next()
		if !ok {
			break
		}
		if event.Action != nil && event.Action.Interrupted != nil {
			interruptEvent = event
			continue
		}
		normalEvents = append(normalEvents, event)
	}
	return
}

type returnDirectlyTool struct {
	name string
}

func (t *returnDirectlyTool) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: t.name,
		Desc: "A tool that returns directly",
	}, nil
}

func (t *returnDirectlyTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) {
	return "return directly result", nil
}

type interruptingTool struct {
	name string
}

func (i *interruptingTool) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: i.name,
		Desc: "A tool that interrupts",
	}, nil
}

func (i *interruptingTool) InvokableRun(ctx context.Context, _ string, _ ...tool.Option) (string, error) {
	if wasInterrupted, _, _ := compose.GetInterruptState[any](ctx); !wasInterrupted {
		return "", compose.Interrupt(ctx, "interrupt data")
	}

	if isResumeFlow, hasResumeData, data := compose.GetResumeContext[string](ctx); isResumeFlow && hasResumeData {
		return data, nil
	}

	return "resumed without data", nil
}

type twoToolCallModel struct {
	returnDirectlyToolName string
	interruptingToolName   string
	callCount              int
	receivedTools          []*schema.ToolInfo
	mu                     sync.Mutex
}

func (m *twoToolCallModel) Generate(_ context.Context, _ []*schema.Message, opts ...model.Option) (*schema.Message, error) {
	m.mu.Lock()
	m.callCount++
	callNum := m.callCount
	options := model.GetCommonOptions(&model.Options{}, opts...)
	if options.Tools != nil {
		m.receivedTools = options.Tools
	}
	m.mu.Unlock()

	if callNum == 1 {
		return &schema.Message{
			Role:    schema.Assistant,
			Content: "",
			ToolCalls: []schema.ToolCall{
				{
					ID:   "call_return_directly",
					Type: "function",
					Function: schema.FunctionCall{
						Name:      m.returnDirectlyToolName,
						Arguments: "{}",
					},
				},
				{
					ID:   "call_interrupting",
					Type: "function",
					Function: schema.FunctionCall{
						Name:      m.interruptingToolName,
						Arguments: "{}",
					},
				},
			},
		}, nil
	}
	return schema.AssistantMessage("final response", nil), nil
}

func (m *twoToolCallModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
	panic("not implemented")
}

func (m *twoToolCallModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
	return m, nil
}

func (m *twoToolCallModel) GetReceivedTools() []*schema.ToolInfo {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.receivedTools
}

type dynamicTool struct {
	name string
}

func (t *dynamicTool) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: t.name,
		Desc: "A dynamically added tool",
	}, nil
}

func (t *dynamicTool) InvokableRun(_ context.Context, _ string, _ ...tool.Option) (string, error) {
	return "dynamic tool result", nil
}

func TestReturnDirectlyEventSentAfterResume(t *testing.T) {
	ctx := context.Background()

	returnDirectlyToolName := "return_directly_tool"
	interruptingToolName := "interrupting_tool"
	dynamicToolName := "dynamic_tool"

	mdl := &twoToolCallModel{
		returnDirectlyToolName: returnDirectlyToolName,
		interruptingToolName:   interruptingToolName,
	}

	agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "TestAgent",
		Description: "Test agent for return directly + interrupt",
		Model:       mdl,
		ToolsConfig: ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{
					&returnDirectlyTool{name: returnDirectlyToolName},
					&interruptingTool{name: interruptingToolName},
				},
			},
			ReturnDirectly: map[string]bool{
				returnDirectlyToolName: true,
			},
		},
		Handlers: []ChatModelAgentMiddleware{
			&interruptTestToolsHandler{tools: []tool.BaseTool{&dynamicTool{name: dynamicToolName}}},
		},
	})
	assert.NoError(t, err)

	store := newMyStore()
	runner := NewRunner(ctx, RunnerConfig{
		Agent:           agent,
		EnableStreaming: false,
		CheckPointStore: store,
	})

	iter := runner.Query(ctx, "test input", WithCheckPointID("test_checkpoint"))

	var interruptEvent *AgentEvent
	for {
		event, ok := iter.Next()
		if !ok {
			break
		}
		if event.Action != nil && event.Action.Interrupted != nil {
			interruptEvent = event
		}
	}

	assert.NotNil(t, interruptEvent, "Should have an interrupt event")
	assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts)

	receivedToolsBeforeResume := mdl.GetReceivedTools()
	var hasDynamicToolBeforeResume bool
	for _, ti := range receivedToolsBeforeResume {
		if ti.Name == dynamicToolName {
			hasDynamicToolBeforeResume = true
		}
	}
	assert.True(t, hasDynamicToolBeforeResume, "Dynamic tool should be in tool list before interrupt")

	interruptID := interruptEvent.Action.Interrupted.InterruptContexts[0].ID
	resumeIter, err := runner.ResumeWithParams(ctx, "test_checkpoint", &ResumeParams{
		Targets: map[string]any{
			interruptID: "resume data",
		},
	})
	assert.NoError(t, err)

	var resumeEvents []*AgentEvent
	for {
		event, ok := resumeIter.Next()
		if !ok {
			break
		}
		resumeEvents = append(resumeEvents, event)
	}

	var hasReturnDirectlyEvent bool
	for _, e := range resumeEvents {
		if e.Output != nil && e.Output.MessageOutput != nil {
			if e.Output.MessageOutput.Role == schema.Tool && e.Output.MessageOutput.ToolName == returnDirectlyToolName {
				hasReturnDirectlyEvent = true
			}
		}
	}
	assert.True(t, hasReturnDirectlyEvent, "ReturnDirectlyEvent should be sent after resume")

	receivedToolsAfterResume := mdl.GetReceivedTools()
	var hasDynamicToolAfterResume bool
	for _, ti := range receivedToolsAfterResume {
		if ti.Name == dynamicToolName {
			hasDynamicToolAfterResume = true
		}
	}
	assert.True(t, hasDynamicToolAfterResume, "Dynamic tool should be in tool list after resume (bc.toolUpdated path)")
}

// streamErrorThenToolCallModel simulates a model that:
// - On the first Stream call: emits several good chunks then an error (triggering retry)
// - On the second Stream call (retry): returns a tool call message (success)
type streamErrorThenToolCallModel struct {
	callCount    int32
	toolCallName string
}

func (m *streamErrorThenToolCallModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
	return schema.AssistantMessage("final answer", nil), nil
}

func (m *streamErrorThenToolCallModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
	count := atomic.AddInt32(&m.callCount, 1)

	sr, sw := schema.Pipe[*schema.Message](10)
	go func() {
		defer sw.Close()
		if count == 1 {
			// First call: emit good chunks then error
			sw.Send(schema.AssistantMessage("chunk1", nil), nil)
			sw.Send(schema.AssistantMessage("chunk2", nil), nil)
			sw.Send(schema.AssistantMessage("chunk3", nil), nil)
			sw.Send(nil, errRetryAble)
			return
		}
		// Second call (retry): return tool call
		sw.Send(schema.AssistantMessage("", []schema.ToolCall{{
			ID:       "call-1",
			Function: schema.FunctionCall{Name: m.toolCallName, Arguments: "{}"},
		}}), nil)
	}()
	return sr, nil
}

func (m *streamErrorThenToolCallModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
	return m, nil
}

// TestStreamRetryThenToolInterruptCheckpoint reproduces a bug where:
//  1. ChatModelAgent with ModelRetryConfig.MaxRetries = 2
//  2. First model Stream call emits good chunks then a retryable error
//  3. Retry succeeds, model returns a tool call
//  4. The tool triggers an interrupt, causing the Runner to save a checkpoint
//  5. The checkpoint save fails because the first (failed) model call's stream event
//     is in the session, and when MessageVariant.GobEncode consumes the stream,
//     it hits the error chunk and returns an encoding error.
func TestStreamRetryThenToolInterruptCheckpoint(t *testing.T) {
	ctx := context.Background()

	interruptToolName := "interrupt_tool"
	mdl := &streamErrorThenToolCallModel{toolCallName: interruptToolName}

	interruptTool := &interruptingTool{name: interruptToolName}

	agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
		Name:        "RetryInterruptAgent",
		Description: "Agent that retries model then tool interrupts",
		Instruction: "You are a test agent.",
		Model:       mdl,
		ModelRetryConfig: &ModelRetryConfig{
			MaxRetries:  2,
			IsRetryAble: func(ctx context.Context, err error) bool { return errors.Is(err, errRetryAble) },
		},
		ToolsConfig: ToolsConfig{
			ToolsNodeConfig: compose.ToolsNodeConfig{
				Tools: []tool.BaseTool{interruptTool},
			},
		},
	})
	assert.NoError(t, err)

	store := newMyStore()
	runner := NewRunner(ctx, RunnerConfig{
		Agent:           agent,
		EnableStreaming: true,
		CheckPointStore: store,
	})

	iter := runner.Run(ctx, []Message{schema.UserMessage("test query")}, WithCheckPointID("retry_interrupt_ckpt"))

	var events []*AgentEvent
	var checkpointErr error
	for {
		event, ok := iter.Next()
		if !ok {
			break
		}
		events = append(events, event)
		if event.Err != nil {
			checkpointErr = event.Err
			t.Logf("event error: %v", event.Err)
		}
	}

	// The bug: checkpoint save fails because the failed stream's error chunk
	// is encountered during gob encoding of the session events.
	// If the bug is fixed, checkpointErr should be nil and we should see an interrupt event.
	assert.NoError(t, checkpointErr, "checkpoint save should not fail due to failed stream's error in session")

	var hasInterrupt bool
	for _, event := range events {
		if event.Action != nil && event.Action.Interrupted != nil {
			hasInterrupt = true
		}
	}
	assert.True(t, hasInterrupt, "should receive an interrupt event from the tool")

	// Verify the model was called twice (first call errored, second succeeded)
	assert.Equal(t, int32(2), atomic.LoadInt32(&mdl.callCount), "model should be called exactly twice (1 failure + 1 retry)")
}
